Spaces:
Configuration error
Configuration error
Upload folder using huggingface_hub (part 13)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_highlight.py +221 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_html.py +1038 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_matplotlib.py +305 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_non_unique.py +140 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_style.py +1602 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_to_latex.py +1091 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_to_string.py +96 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_to_typst.py +96 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_tooltip.py +179 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_console.py +72 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_css.py +288 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_eng_formatting.py +254 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_format.py +2302 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_ipython_compat.py +91 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_printing.py +172 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_csv.py +886 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_excel.py +473 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_html.py +1166 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_latex.py +1527 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_markdown.py +102 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_string.py +1226 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/generate_legacy_storage_files.py +421 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/json/__init__.py +0 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/json/conftest.py +9 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/json/test_compression.py +130 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/json/test_deprecated_kwargs.py +22 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/json/test_json_table_schema.py +898 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/json/test_json_table_schema_ext_dtype.py +296 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/json/test_normalize.py +942 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/json/test_pandas.py +2387 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/json/test_readlines.py +544 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/json/test_ujson.py +1045 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/parser/__init__.py +0 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/__init__.py +0 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_chunksize.py +364 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_common_basic.py +868 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_data_list.py +92 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_decimal.py +73 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_file_buffer_url.py +470 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_float.py +110 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_index.py +307 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_inf.py +59 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_ints.py +244 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_iterator.py +159 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_read_errors.py +315 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/parser/conftest.py +337 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/__init__.py +0 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/test_categorical.py +338 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/test_dtypes_basic.py +639 -0
- .venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/test_empty.py +181 -0
.venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_highlight.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pytest
|
| 3 |
+
|
| 4 |
+
from pandas import (
|
| 5 |
+
NA,
|
| 6 |
+
DataFrame,
|
| 7 |
+
IndexSlice,
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
pytest.importorskip("jinja2")
|
| 11 |
+
|
| 12 |
+
from pandas.io.formats.style import Styler
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@pytest.fixture(params=[(None, "float64"), (NA, "Int64")])
|
| 16 |
+
def df(request):
|
| 17 |
+
# GH 45804
|
| 18 |
+
dtype = request.param[1]
|
| 19 |
+
item = np.nan if dtype == "float64" else NA
|
| 20 |
+
return DataFrame(
|
| 21 |
+
{"A": [0, item, 10], "B": [1, request.param[0], 2]}, dtype=request.param[1]
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@pytest.fixture
|
| 26 |
+
def styler(df):
|
| 27 |
+
return Styler(df, uuid_len=0)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def test_highlight_null(styler):
|
| 31 |
+
result = styler.highlight_null()._compute().ctx
|
| 32 |
+
expected = {
|
| 33 |
+
(1, 0): [("background-color", "red")],
|
| 34 |
+
(1, 1): [("background-color", "red")],
|
| 35 |
+
}
|
| 36 |
+
assert result == expected
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def test_highlight_null_subset(styler):
|
| 40 |
+
# GH 31345
|
| 41 |
+
result = (
|
| 42 |
+
styler.highlight_null(color="red", subset=["A"])
|
| 43 |
+
.highlight_null(color="green", subset=["B"])
|
| 44 |
+
._compute()
|
| 45 |
+
.ctx
|
| 46 |
+
)
|
| 47 |
+
expected = {
|
| 48 |
+
(1, 0): [("background-color", "red")],
|
| 49 |
+
(1, 1): [("background-color", "green")],
|
| 50 |
+
}
|
| 51 |
+
assert result == expected
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
@pytest.mark.parametrize("f", ["highlight_min", "highlight_max"])
|
| 55 |
+
def test_highlight_minmax_basic(df, f):
|
| 56 |
+
expected = {
|
| 57 |
+
(0, 1): [("background-color", "red")],
|
| 58 |
+
# ignores NaN row,
|
| 59 |
+
(2, 0): [("background-color", "red")],
|
| 60 |
+
}
|
| 61 |
+
if f == "highlight_min":
|
| 62 |
+
df = -df
|
| 63 |
+
result = getattr(df.style, f)(axis=1, color="red")._compute().ctx
|
| 64 |
+
assert result == expected
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
@pytest.mark.parametrize("f", ["highlight_min", "highlight_max"])
|
| 68 |
+
@pytest.mark.parametrize(
|
| 69 |
+
"kwargs",
|
| 70 |
+
[
|
| 71 |
+
{"axis": None, "color": "red"}, # test axis
|
| 72 |
+
{"axis": 0, "subset": ["A"], "color": "red"}, # test subset and ignores NaN
|
| 73 |
+
{"axis": None, "props": "background-color: red"}, # test props
|
| 74 |
+
],
|
| 75 |
+
)
|
| 76 |
+
def test_highlight_minmax_ext(df, f, kwargs):
|
| 77 |
+
expected = {(2, 0): [("background-color", "red")]}
|
| 78 |
+
if f == "highlight_min":
|
| 79 |
+
df = -df
|
| 80 |
+
result = getattr(df.style, f)(**kwargs)._compute().ctx
|
| 81 |
+
assert result == expected
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@pytest.mark.parametrize("f", ["highlight_min", "highlight_max"])
|
| 85 |
+
@pytest.mark.parametrize("axis", [None, 0, 1])
|
| 86 |
+
def test_highlight_minmax_nulls(f, axis):
|
| 87 |
+
# GH 42750
|
| 88 |
+
expected = {
|
| 89 |
+
(1, 0): [("background-color", "yellow")],
|
| 90 |
+
(1, 1): [("background-color", "yellow")],
|
| 91 |
+
}
|
| 92 |
+
if axis == 1:
|
| 93 |
+
expected.update({(2, 1): [("background-color", "yellow")]})
|
| 94 |
+
|
| 95 |
+
if f == "highlight_max":
|
| 96 |
+
df = DataFrame({"a": [NA, 1, None], "b": [np.nan, 1, -1]})
|
| 97 |
+
else:
|
| 98 |
+
df = DataFrame({"a": [NA, -1, None], "b": [np.nan, -1, 1]})
|
| 99 |
+
|
| 100 |
+
result = getattr(df.style, f)(axis=axis)._compute().ctx
|
| 101 |
+
assert result == expected
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
@pytest.mark.parametrize(
|
| 105 |
+
"kwargs",
|
| 106 |
+
[
|
| 107 |
+
{"left": 0, "right": 1}, # test basic range
|
| 108 |
+
{"left": 0, "right": 1, "props": "background-color: yellow"}, # test props
|
| 109 |
+
{"left": -100, "right": 100, "subset": IndexSlice[[0, 1], :]}, # test subset
|
| 110 |
+
{"left": 0, "subset": IndexSlice[[0, 1], :]}, # test no right
|
| 111 |
+
{"right": 1}, # test no left
|
| 112 |
+
{"left": [0, 0, 11], "axis": 0}, # test left as sequence
|
| 113 |
+
{"left": DataFrame({"A": [0, 0, 11], "B": [1, 1, 11]}), "axis": None}, # axis
|
| 114 |
+
{"left": 0, "right": [0, 1], "axis": 1}, # test sequence right
|
| 115 |
+
],
|
| 116 |
+
)
|
| 117 |
+
def test_highlight_between(styler, kwargs):
|
| 118 |
+
expected = {
|
| 119 |
+
(0, 0): [("background-color", "yellow")],
|
| 120 |
+
(0, 1): [("background-color", "yellow")],
|
| 121 |
+
}
|
| 122 |
+
result = styler.highlight_between(**kwargs)._compute().ctx
|
| 123 |
+
assert result == expected
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
@pytest.mark.parametrize(
|
| 127 |
+
"arg, map, axis",
|
| 128 |
+
[
|
| 129 |
+
("left", [1, 2], 0), # 0 axis has 3 elements not 2
|
| 130 |
+
("left", [1, 2, 3], 1), # 1 axis has 2 elements not 3
|
| 131 |
+
("left", np.array([[1, 2], [1, 2]]), None), # df is (2,3) not (2,2)
|
| 132 |
+
("right", [1, 2], 0), # same tests as above for 'right' not 'left'
|
| 133 |
+
("right", [1, 2, 3], 1), # ..
|
| 134 |
+
("right", np.array([[1, 2], [1, 2]]), None), # ..
|
| 135 |
+
],
|
| 136 |
+
)
|
| 137 |
+
def test_highlight_between_raises(arg, styler, map, axis):
|
| 138 |
+
msg = f"supplied '{arg}' is not correct shape"
|
| 139 |
+
with pytest.raises(ValueError, match=msg):
|
| 140 |
+
styler.highlight_between(**{arg: map, "axis": axis})._compute()
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def test_highlight_between_raises2(styler):
|
| 144 |
+
msg = "values can be 'both', 'left', 'right', or 'neither'"
|
| 145 |
+
with pytest.raises(ValueError, match=msg):
|
| 146 |
+
styler.highlight_between(inclusive="badstring")._compute()
|
| 147 |
+
|
| 148 |
+
with pytest.raises(ValueError, match=msg):
|
| 149 |
+
styler.highlight_between(inclusive=1)._compute()
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
@pytest.mark.parametrize(
|
| 153 |
+
"inclusive, expected",
|
| 154 |
+
[
|
| 155 |
+
(
|
| 156 |
+
"both",
|
| 157 |
+
{
|
| 158 |
+
(0, 0): [("background-color", "yellow")],
|
| 159 |
+
(0, 1): [("background-color", "yellow")],
|
| 160 |
+
},
|
| 161 |
+
),
|
| 162 |
+
("neither", {}),
|
| 163 |
+
("left", {(0, 0): [("background-color", "yellow")]}),
|
| 164 |
+
("right", {(0, 1): [("background-color", "yellow")]}),
|
| 165 |
+
],
|
| 166 |
+
)
|
| 167 |
+
def test_highlight_between_inclusive(styler, inclusive, expected):
|
| 168 |
+
kwargs = {"left": 0, "right": 1, "subset": IndexSlice[[0, 1], :]}
|
| 169 |
+
result = styler.highlight_between(**kwargs, inclusive=inclusive)._compute()
|
| 170 |
+
assert result.ctx == expected
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
@pytest.mark.parametrize(
|
| 174 |
+
"kwargs",
|
| 175 |
+
[
|
| 176 |
+
{"q_left": 0.5, "q_right": 1, "axis": 0}, # base case
|
| 177 |
+
{"q_left": 0.5, "q_right": 1, "axis": None}, # test axis
|
| 178 |
+
{"q_left": 0, "q_right": 1, "subset": IndexSlice[2, :]}, # test subset
|
| 179 |
+
{"q_left": 0.5, "axis": 0}, # test no high
|
| 180 |
+
{"q_right": 1, "subset": IndexSlice[2, :], "axis": 1}, # test no low
|
| 181 |
+
{"q_left": 0.5, "axis": 0, "props": "background-color: yellow"}, # tst prop
|
| 182 |
+
],
|
| 183 |
+
)
|
| 184 |
+
def test_highlight_quantile(styler, kwargs):
|
| 185 |
+
expected = {
|
| 186 |
+
(2, 0): [("background-color", "yellow")],
|
| 187 |
+
(2, 1): [("background-color", "yellow")],
|
| 188 |
+
}
|
| 189 |
+
result = styler.highlight_quantile(**kwargs)._compute().ctx
|
| 190 |
+
assert result == expected
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
@pytest.mark.parametrize(
|
| 194 |
+
"f,kwargs",
|
| 195 |
+
[
|
| 196 |
+
("highlight_min", {"axis": 1, "subset": IndexSlice[1, :]}),
|
| 197 |
+
("highlight_max", {"axis": 0, "subset": [0]}),
|
| 198 |
+
("highlight_quantile", {"axis": None, "q_left": 0.6, "q_right": 0.8}),
|
| 199 |
+
("highlight_between", {"subset": [0]}),
|
| 200 |
+
],
|
| 201 |
+
)
|
| 202 |
+
@pytest.mark.parametrize(
|
| 203 |
+
"dtype",
|
| 204 |
+
[
|
| 205 |
+
int,
|
| 206 |
+
float,
|
| 207 |
+
"datetime64[ns]",
|
| 208 |
+
str,
|
| 209 |
+
"timedelta64[ns]",
|
| 210 |
+
],
|
| 211 |
+
)
|
| 212 |
+
def test_all_highlight_dtypes(f, kwargs, dtype):
|
| 213 |
+
df = DataFrame([[0, 10], [20, 30]], dtype=dtype)
|
| 214 |
+
if f == "highlight_quantile" and isinstance(df.iloc[0, 0], (str)):
|
| 215 |
+
return None # quantile incompatible with str
|
| 216 |
+
if f == "highlight_between":
|
| 217 |
+
kwargs["left"] = df.iloc[1, 0] # set the range low for testing
|
| 218 |
+
|
| 219 |
+
expected = {(1, 0): [("background-color", "yellow")]}
|
| 220 |
+
result = getattr(df.style, f)(**kwargs)._compute().ctx
|
| 221 |
+
assert result == expected
|
.venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_html.py
ADDED
|
@@ -0,0 +1,1038 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pathlib
|
| 2 |
+
from textwrap import (
|
| 3 |
+
dedent,
|
| 4 |
+
indent,
|
| 5 |
+
)
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import pytest
|
| 9 |
+
|
| 10 |
+
from pandas import (
|
| 11 |
+
DataFrame,
|
| 12 |
+
MultiIndex,
|
| 13 |
+
option_context,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
jinja2 = pytest.importorskip("jinja2")
|
| 17 |
+
from pandas.io.formats.style import Styler
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@pytest.fixture
|
| 21 |
+
def env():
|
| 22 |
+
project_dir = pathlib.Path(__file__).parent.parent.parent.parent.parent.resolve()
|
| 23 |
+
template_dir = project_dir / "io" / "formats" / "templates"
|
| 24 |
+
loader = jinja2.FileSystemLoader(template_dir)
|
| 25 |
+
env = jinja2.Environment(loader=loader, trim_blocks=True)
|
| 26 |
+
return env
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@pytest.fixture
|
| 30 |
+
def styler():
|
| 31 |
+
return Styler(DataFrame([[2.61], [2.69]], index=["a", "b"], columns=["A"]))
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@pytest.fixture
|
| 35 |
+
def styler_mi():
|
| 36 |
+
midx = MultiIndex.from_product([["a", "b"], ["c", "d"]])
|
| 37 |
+
return Styler(DataFrame(np.arange(16).reshape(4, 4), index=midx, columns=midx))
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@pytest.fixture
|
| 41 |
+
def styler_multi():
|
| 42 |
+
df = DataFrame(
|
| 43 |
+
data=np.arange(16).reshape(4, 4),
|
| 44 |
+
columns=MultiIndex.from_product([["A", "B"], ["a", "b"]], names=["A&", "b&"]),
|
| 45 |
+
index=MultiIndex.from_product([["X", "Y"], ["x", "y"]], names=["X>", "y_"]),
|
| 46 |
+
)
|
| 47 |
+
return Styler(df)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
@pytest.fixture
|
| 51 |
+
def tpl_style(env):
|
| 52 |
+
return env.get_template("html_style.tpl")
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@pytest.fixture
|
| 56 |
+
def tpl_table(env):
|
| 57 |
+
return env.get_template("html_table.tpl")
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def test_html_template_extends_options(datapath):
|
| 61 |
+
# make sure if templates are edited tests are updated as are setup fixtures
|
| 62 |
+
# to understand the dependency
|
| 63 |
+
path = datapath("..", "io", "formats", "templates", "html.tpl")
|
| 64 |
+
with open(path, encoding="utf-8") as file:
|
| 65 |
+
result = file.read()
|
| 66 |
+
assert "{% include html_style_tpl %}" in result
|
| 67 |
+
assert "{% include html_table_tpl %}" in result
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def test_exclude_styles(styler):
|
| 71 |
+
result = styler.to_html(exclude_styles=True, doctype_html=True)
|
| 72 |
+
expected = dedent(
|
| 73 |
+
"""\
|
| 74 |
+
<!DOCTYPE html>
|
| 75 |
+
<html>
|
| 76 |
+
<head>
|
| 77 |
+
<meta charset="utf-8">
|
| 78 |
+
</head>
|
| 79 |
+
<body>
|
| 80 |
+
<table>
|
| 81 |
+
<thead>
|
| 82 |
+
<tr>
|
| 83 |
+
<th > </th>
|
| 84 |
+
<th >A</th>
|
| 85 |
+
</tr>
|
| 86 |
+
</thead>
|
| 87 |
+
<tbody>
|
| 88 |
+
<tr>
|
| 89 |
+
<th >a</th>
|
| 90 |
+
<td >2.610000</td>
|
| 91 |
+
</tr>
|
| 92 |
+
<tr>
|
| 93 |
+
<th >b</th>
|
| 94 |
+
<td >2.690000</td>
|
| 95 |
+
</tr>
|
| 96 |
+
</tbody>
|
| 97 |
+
</table>
|
| 98 |
+
</body>
|
| 99 |
+
</html>
|
| 100 |
+
"""
|
| 101 |
+
)
|
| 102 |
+
assert result == expected
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def test_w3_html_format(styler):
|
| 106 |
+
styler.set_uuid("").set_table_styles([{"selector": "th", "props": "att2:v2;"}]).map(
|
| 107 |
+
lambda x: "att1:v1;"
|
| 108 |
+
).set_table_attributes('class="my-cls1" style="attr3:v3;"').set_td_classes(
|
| 109 |
+
DataFrame(["my-cls2"], index=["a"], columns=["A"])
|
| 110 |
+
).format("{:.1f}").set_caption("A comprehensive test")
|
| 111 |
+
expected = dedent(
|
| 112 |
+
"""\
|
| 113 |
+
<style type="text/css">
|
| 114 |
+
#T_ th {
|
| 115 |
+
att2: v2;
|
| 116 |
+
}
|
| 117 |
+
#T__row0_col0, #T__row1_col0 {
|
| 118 |
+
att1: v1;
|
| 119 |
+
}
|
| 120 |
+
</style>
|
| 121 |
+
<table id="T_" class="my-cls1" style="attr3:v3;">
|
| 122 |
+
<caption>A comprehensive test</caption>
|
| 123 |
+
<thead>
|
| 124 |
+
<tr>
|
| 125 |
+
<th class="blank level0" > </th>
|
| 126 |
+
<th id="T__level0_col0" class="col_heading level0 col0" >A</th>
|
| 127 |
+
</tr>
|
| 128 |
+
</thead>
|
| 129 |
+
<tbody>
|
| 130 |
+
<tr>
|
| 131 |
+
<th id="T__level0_row0" class="row_heading level0 row0" >a</th>
|
| 132 |
+
<td id="T__row0_col0" class="data row0 col0 my-cls2" >2.6</td>
|
| 133 |
+
</tr>
|
| 134 |
+
<tr>
|
| 135 |
+
<th id="T__level0_row1" class="row_heading level0 row1" >b</th>
|
| 136 |
+
<td id="T__row1_col0" class="data row1 col0" >2.7</td>
|
| 137 |
+
</tr>
|
| 138 |
+
</tbody>
|
| 139 |
+
</table>
|
| 140 |
+
"""
|
| 141 |
+
)
|
| 142 |
+
assert expected == styler.to_html()
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def test_colspan_w3():
|
| 146 |
+
# GH 36223
|
| 147 |
+
df = DataFrame(data=[[1, 2]], columns=[["l0", "l0"], ["l1a", "l1b"]])
|
| 148 |
+
styler = Styler(df, uuid="_", cell_ids=False)
|
| 149 |
+
assert '<th class="col_heading level0 col0" colspan="2">l0</th>' in styler.to_html()
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def test_rowspan_w3():
|
| 153 |
+
# GH 38533
|
| 154 |
+
df = DataFrame(data=[[1, 2]], index=[["l0", "l0"], ["l1a", "l1b"]])
|
| 155 |
+
styler = Styler(df, uuid="_", cell_ids=False)
|
| 156 |
+
assert '<th class="row_heading level0 row0" rowspan="2">l0</th>' in styler.to_html()
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def test_styles(styler):
|
| 160 |
+
styler.set_uuid("abc")
|
| 161 |
+
styler.set_table_styles([{"selector": "td", "props": "color: red;"}])
|
| 162 |
+
result = styler.to_html(doctype_html=True)
|
| 163 |
+
expected = dedent(
|
| 164 |
+
"""\
|
| 165 |
+
<!DOCTYPE html>
|
| 166 |
+
<html>
|
| 167 |
+
<head>
|
| 168 |
+
<meta charset="utf-8">
|
| 169 |
+
<style type="text/css">
|
| 170 |
+
#T_abc td {
|
| 171 |
+
color: red;
|
| 172 |
+
}
|
| 173 |
+
</style>
|
| 174 |
+
</head>
|
| 175 |
+
<body>
|
| 176 |
+
<table id="T_abc">
|
| 177 |
+
<thead>
|
| 178 |
+
<tr>
|
| 179 |
+
<th class="blank level0" > </th>
|
| 180 |
+
<th id="T_abc_level0_col0" class="col_heading level0 col0" >A</th>
|
| 181 |
+
</tr>
|
| 182 |
+
</thead>
|
| 183 |
+
<tbody>
|
| 184 |
+
<tr>
|
| 185 |
+
<th id="T_abc_level0_row0" class="row_heading level0 row0" >a</th>
|
| 186 |
+
<td id="T_abc_row0_col0" class="data row0 col0" >2.610000</td>
|
| 187 |
+
</tr>
|
| 188 |
+
<tr>
|
| 189 |
+
<th id="T_abc_level0_row1" class="row_heading level0 row1" >b</th>
|
| 190 |
+
<td id="T_abc_row1_col0" class="data row1 col0" >2.690000</td>
|
| 191 |
+
</tr>
|
| 192 |
+
</tbody>
|
| 193 |
+
</table>
|
| 194 |
+
</body>
|
| 195 |
+
</html>
|
| 196 |
+
"""
|
| 197 |
+
)
|
| 198 |
+
assert result == expected
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def test_doctype(styler):
|
| 202 |
+
result = styler.to_html(doctype_html=False)
|
| 203 |
+
assert "<html>" not in result
|
| 204 |
+
assert "<body>" not in result
|
| 205 |
+
assert "<!DOCTYPE html>" not in result
|
| 206 |
+
assert "<head>" not in result
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def test_doctype_encoding(styler):
|
| 210 |
+
with option_context("styler.render.encoding", "ASCII"):
|
| 211 |
+
result = styler.to_html(doctype_html=True)
|
| 212 |
+
assert '<meta charset="ASCII">' in result
|
| 213 |
+
result = styler.to_html(doctype_html=True, encoding="ANSI")
|
| 214 |
+
assert '<meta charset="ANSI">' in result
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def test_bold_headers_arg(styler):
|
| 218 |
+
result = styler.to_html(bold_headers=True)
|
| 219 |
+
assert "th {\n font-weight: bold;\n}" in result
|
| 220 |
+
result = styler.to_html()
|
| 221 |
+
assert "th {\n font-weight: bold;\n}" not in result
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def test_caption_arg(styler):
|
| 225 |
+
result = styler.to_html(caption="foo bar")
|
| 226 |
+
assert "<caption>foo bar</caption>" in result
|
| 227 |
+
result = styler.to_html()
|
| 228 |
+
assert "<caption>foo bar</caption>" not in result
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def test_block_names(tpl_style, tpl_table):
|
| 232 |
+
# catch accidental removal of a block
|
| 233 |
+
expected_style = {
|
| 234 |
+
"before_style",
|
| 235 |
+
"style",
|
| 236 |
+
"table_styles",
|
| 237 |
+
"before_cellstyle",
|
| 238 |
+
"cellstyle",
|
| 239 |
+
}
|
| 240 |
+
expected_table = {
|
| 241 |
+
"before_table",
|
| 242 |
+
"table",
|
| 243 |
+
"caption",
|
| 244 |
+
"thead",
|
| 245 |
+
"tbody",
|
| 246 |
+
"after_table",
|
| 247 |
+
"before_head_rows",
|
| 248 |
+
"head_tr",
|
| 249 |
+
"after_head_rows",
|
| 250 |
+
"before_rows",
|
| 251 |
+
"tr",
|
| 252 |
+
"after_rows",
|
| 253 |
+
}
|
| 254 |
+
result1 = set(tpl_style.blocks)
|
| 255 |
+
assert result1 == expected_style
|
| 256 |
+
|
| 257 |
+
result2 = set(tpl_table.blocks)
|
| 258 |
+
assert result2 == expected_table
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def test_from_custom_template_table(tmpdir):
|
| 262 |
+
p = tmpdir.mkdir("tpl").join("myhtml_table.tpl")
|
| 263 |
+
p.write(
|
| 264 |
+
dedent(
|
| 265 |
+
"""\
|
| 266 |
+
{% extends "html_table.tpl" %}
|
| 267 |
+
{% block table %}
|
| 268 |
+
<h1>{{custom_title}}</h1>
|
| 269 |
+
{{ super() }}
|
| 270 |
+
{% endblock table %}"""
|
| 271 |
+
)
|
| 272 |
+
)
|
| 273 |
+
result = Styler.from_custom_template(str(tmpdir.join("tpl")), "myhtml_table.tpl")
|
| 274 |
+
assert issubclass(result, Styler)
|
| 275 |
+
assert result.env is not Styler.env
|
| 276 |
+
assert result.template_html_table is not Styler.template_html_table
|
| 277 |
+
styler = result(DataFrame({"A": [1, 2]}))
|
| 278 |
+
assert "<h1>My Title</h1>\n\n\n<table" in styler.to_html(custom_title="My Title")
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def test_from_custom_template_style(tmpdir):
|
| 282 |
+
p = tmpdir.mkdir("tpl").join("myhtml_style.tpl")
|
| 283 |
+
p.write(
|
| 284 |
+
dedent(
|
| 285 |
+
"""\
|
| 286 |
+
{% extends "html_style.tpl" %}
|
| 287 |
+
{% block style %}
|
| 288 |
+
<link rel="stylesheet" href="mystyle.css">
|
| 289 |
+
{{ super() }}
|
| 290 |
+
{% endblock style %}"""
|
| 291 |
+
)
|
| 292 |
+
)
|
| 293 |
+
result = Styler.from_custom_template(
|
| 294 |
+
str(tmpdir.join("tpl")), html_style="myhtml_style.tpl"
|
| 295 |
+
)
|
| 296 |
+
assert issubclass(result, Styler)
|
| 297 |
+
assert result.env is not Styler.env
|
| 298 |
+
assert result.template_html_style is not Styler.template_html_style
|
| 299 |
+
styler = result(DataFrame({"A": [1, 2]}))
|
| 300 |
+
assert '<link rel="stylesheet" href="mystyle.css">\n\n<style' in styler.to_html()
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
def test_caption_as_sequence(styler):
|
| 304 |
+
styler.set_caption(("full cap", "short cap"))
|
| 305 |
+
assert "<caption>full cap</caption>" in styler.to_html()
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
@pytest.mark.parametrize("index", [False, True])
|
| 309 |
+
@pytest.mark.parametrize("columns", [False, True])
|
| 310 |
+
@pytest.mark.parametrize("index_name", [True, False])
|
| 311 |
+
def test_sticky_basic(styler, index, columns, index_name):
|
| 312 |
+
if index_name:
|
| 313 |
+
styler.index.name = "some text"
|
| 314 |
+
if index:
|
| 315 |
+
styler.set_sticky(axis=0)
|
| 316 |
+
if columns:
|
| 317 |
+
styler.set_sticky(axis=1)
|
| 318 |
+
|
| 319 |
+
left_css = (
|
| 320 |
+
"#T_ {0} {{\n position: sticky;\n background-color: inherit;\n"
|
| 321 |
+
" left: 0px;\n z-index: {1};\n}}"
|
| 322 |
+
)
|
| 323 |
+
top_css = (
|
| 324 |
+
"#T_ {0} {{\n position: sticky;\n background-color: inherit;\n"
|
| 325 |
+
" top: {1}px;\n z-index: {2};\n{3}}}"
|
| 326 |
+
)
|
| 327 |
+
|
| 328 |
+
res = styler.set_uuid("").to_html()
|
| 329 |
+
|
| 330 |
+
# test index stickys over thead and tbody
|
| 331 |
+
assert (left_css.format("thead tr th:nth-child(1)", "3 !important") in res) is index
|
| 332 |
+
assert (left_css.format("tbody tr th:nth-child(1)", "1") in res) is index
|
| 333 |
+
|
| 334 |
+
# test column stickys including if name row
|
| 335 |
+
assert (
|
| 336 |
+
top_css.format("thead tr:nth-child(1) th", "0", "2", " height: 25px;\n") in res
|
| 337 |
+
) is (columns and index_name)
|
| 338 |
+
assert (
|
| 339 |
+
top_css.format("thead tr:nth-child(2) th", "25", "2", " height: 25px;\n")
|
| 340 |
+
in res
|
| 341 |
+
) is (columns and index_name)
|
| 342 |
+
assert (top_css.format("thead tr:nth-child(1) th", "0", "2", "") in res) is (
|
| 343 |
+
columns and not index_name
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
@pytest.mark.parametrize("index", [False, True])
|
| 348 |
+
@pytest.mark.parametrize("columns", [False, True])
|
| 349 |
+
def test_sticky_mi(styler_mi, index, columns):
|
| 350 |
+
if index:
|
| 351 |
+
styler_mi.set_sticky(axis=0)
|
| 352 |
+
if columns:
|
| 353 |
+
styler_mi.set_sticky(axis=1)
|
| 354 |
+
|
| 355 |
+
left_css = (
|
| 356 |
+
"#T_ {0} {{\n position: sticky;\n background-color: inherit;\n"
|
| 357 |
+
" left: {1}px;\n min-width: 75px;\n max-width: 75px;\n z-index: {2};\n}}"
|
| 358 |
+
)
|
| 359 |
+
top_css = (
|
| 360 |
+
"#T_ {0} {{\n position: sticky;\n background-color: inherit;\n"
|
| 361 |
+
" top: {1}px;\n height: 25px;\n z-index: {2};\n}}"
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
res = styler_mi.set_uuid("").to_html()
|
| 365 |
+
|
| 366 |
+
# test the index stickys for thead and tbody over both levels
|
| 367 |
+
assert (
|
| 368 |
+
left_css.format("thead tr th:nth-child(1)", "0", "3 !important") in res
|
| 369 |
+
) is index
|
| 370 |
+
assert (left_css.format("tbody tr th.level0", "0", "1") in res) is index
|
| 371 |
+
assert (
|
| 372 |
+
left_css.format("thead tr th:nth-child(2)", "75", "3 !important") in res
|
| 373 |
+
) is index
|
| 374 |
+
assert (left_css.format("tbody tr th.level1", "75", "1") in res) is index
|
| 375 |
+
|
| 376 |
+
# test the column stickys for each level row
|
| 377 |
+
assert (top_css.format("thead tr:nth-child(1) th", "0", "2") in res) is columns
|
| 378 |
+
assert (top_css.format("thead tr:nth-child(2) th", "25", "2") in res) is columns
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
@pytest.mark.parametrize("index", [False, True])
|
| 382 |
+
@pytest.mark.parametrize("columns", [False, True])
|
| 383 |
+
@pytest.mark.parametrize("levels", [[1], ["one"], "one"])
|
| 384 |
+
def test_sticky_levels(styler_mi, index, columns, levels):
|
| 385 |
+
styler_mi.index.names, styler_mi.columns.names = ["zero", "one"], ["zero", "one"]
|
| 386 |
+
if index:
|
| 387 |
+
styler_mi.set_sticky(axis=0, levels=levels)
|
| 388 |
+
if columns:
|
| 389 |
+
styler_mi.set_sticky(axis=1, levels=levels)
|
| 390 |
+
|
| 391 |
+
left_css = (
|
| 392 |
+
"#T_ {0} {{\n position: sticky;\n background-color: inherit;\n"
|
| 393 |
+
" left: {1}px;\n min-width: 75px;\n max-width: 75px;\n z-index: {2};\n}}"
|
| 394 |
+
)
|
| 395 |
+
top_css = (
|
| 396 |
+
"#T_ {0} {{\n position: sticky;\n background-color: inherit;\n"
|
| 397 |
+
" top: {1}px;\n height: 25px;\n z-index: {2};\n}}"
|
| 398 |
+
)
|
| 399 |
+
|
| 400 |
+
res = styler_mi.set_uuid("").to_html()
|
| 401 |
+
|
| 402 |
+
# test no sticking of level0
|
| 403 |
+
assert "#T_ thead tr th:nth-child(1)" not in res
|
| 404 |
+
assert "#T_ tbody tr th.level0" not in res
|
| 405 |
+
assert "#T_ thead tr:nth-child(1) th" not in res
|
| 406 |
+
|
| 407 |
+
# test sticking level1
|
| 408 |
+
assert (
|
| 409 |
+
left_css.format("thead tr th:nth-child(2)", "0", "3 !important") in res
|
| 410 |
+
) is index
|
| 411 |
+
assert (left_css.format("tbody tr th.level1", "0", "1") in res) is index
|
| 412 |
+
assert (top_css.format("thead tr:nth-child(2) th", "0", "2") in res) is columns
|
| 413 |
+
|
| 414 |
+
|
| 415 |
+
def test_sticky_raises(styler):
|
| 416 |
+
with pytest.raises(ValueError, match="No axis named bad for object type DataFrame"):
|
| 417 |
+
styler.set_sticky(axis="bad")
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
@pytest.mark.parametrize(
|
| 421 |
+
"sparse_index, sparse_columns",
|
| 422 |
+
[(True, True), (True, False), (False, True), (False, False)],
|
| 423 |
+
)
|
| 424 |
+
def test_sparse_options(sparse_index, sparse_columns):
|
| 425 |
+
cidx = MultiIndex.from_tuples([("Z", "a"), ("Z", "b"), ("Y", "c")])
|
| 426 |
+
ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")])
|
| 427 |
+
df = DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]], index=ridx, columns=cidx)
|
| 428 |
+
styler = df.style
|
| 429 |
+
|
| 430 |
+
default_html = styler.to_html() # defaults under pd.options to (True , True)
|
| 431 |
+
|
| 432 |
+
with option_context(
|
| 433 |
+
"styler.sparse.index", sparse_index, "styler.sparse.columns", sparse_columns
|
| 434 |
+
):
|
| 435 |
+
html1 = styler.to_html()
|
| 436 |
+
assert (html1 == default_html) is (sparse_index and sparse_columns)
|
| 437 |
+
html2 = styler.to_html(sparse_index=sparse_index, sparse_columns=sparse_columns)
|
| 438 |
+
assert html1 == html2
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
@pytest.mark.parametrize("index", [True, False])
|
| 442 |
+
@pytest.mark.parametrize("columns", [True, False])
|
| 443 |
+
def test_map_header_cell_ids(styler, index, columns):
|
| 444 |
+
# GH 41893
|
| 445 |
+
func = lambda v: "attr: val;"
|
| 446 |
+
styler.uuid, styler.cell_ids = "", False
|
| 447 |
+
if index:
|
| 448 |
+
styler.map_index(func, axis="index")
|
| 449 |
+
if columns:
|
| 450 |
+
styler.map_index(func, axis="columns")
|
| 451 |
+
|
| 452 |
+
result = styler.to_html()
|
| 453 |
+
|
| 454 |
+
# test no data cell ids
|
| 455 |
+
assert '<td class="data row0 col0" >2.610000</td>' in result
|
| 456 |
+
assert '<td class="data row1 col0" >2.690000</td>' in result
|
| 457 |
+
|
| 458 |
+
# test index header ids where needed and css styles
|
| 459 |
+
assert (
|
| 460 |
+
'<th id="T__level0_row0" class="row_heading level0 row0" >a</th>' in result
|
| 461 |
+
) is index
|
| 462 |
+
assert (
|
| 463 |
+
'<th id="T__level0_row1" class="row_heading level0 row1" >b</th>' in result
|
| 464 |
+
) is index
|
| 465 |
+
assert ("#T__level0_row0, #T__level0_row1 {\n attr: val;\n}" in result) is index
|
| 466 |
+
|
| 467 |
+
# test column header ids where needed and css styles
|
| 468 |
+
assert (
|
| 469 |
+
'<th id="T__level0_col0" class="col_heading level0 col0" >A</th>' in result
|
| 470 |
+
) is columns
|
| 471 |
+
assert ("#T__level0_col0 {\n attr: val;\n}" in result) is columns
|
| 472 |
+
|
| 473 |
+
|
| 474 |
+
@pytest.mark.parametrize("rows", [True, False])
|
| 475 |
+
@pytest.mark.parametrize("cols", [True, False])
|
| 476 |
+
def test_maximums(styler_mi, rows, cols):
|
| 477 |
+
result = styler_mi.to_html(
|
| 478 |
+
max_rows=2 if rows else None,
|
| 479 |
+
max_columns=2 if cols else None,
|
| 480 |
+
)
|
| 481 |
+
|
| 482 |
+
assert ">5</td>" in result # [[0,1], [4,5]] always visible
|
| 483 |
+
assert (">8</td>" in result) is not rows # first trimmed vertical element
|
| 484 |
+
assert (">2</td>" in result) is not cols # first trimmed horizontal element
|
| 485 |
+
|
| 486 |
+
|
| 487 |
+
def test_replaced_css_class_names():
|
| 488 |
+
css = {
|
| 489 |
+
"row_heading": "ROWHEAD",
|
| 490 |
+
# "col_heading": "COLHEAD",
|
| 491 |
+
"index_name": "IDXNAME",
|
| 492 |
+
# "col": "COL",
|
| 493 |
+
"row": "ROW",
|
| 494 |
+
# "col_trim": "COLTRIM",
|
| 495 |
+
"row_trim": "ROWTRIM",
|
| 496 |
+
"level": "LEVEL",
|
| 497 |
+
"data": "DATA",
|
| 498 |
+
"blank": "BLANK",
|
| 499 |
+
}
|
| 500 |
+
midx = MultiIndex.from_product([["a", "b"], ["c", "d"]])
|
| 501 |
+
styler_mi = Styler(
|
| 502 |
+
DataFrame(np.arange(16).reshape(4, 4), index=midx, columns=midx),
|
| 503 |
+
uuid_len=0,
|
| 504 |
+
).set_table_styles(css_class_names=css)
|
| 505 |
+
styler_mi.index.names = ["n1", "n2"]
|
| 506 |
+
styler_mi.hide(styler_mi.index[1:], axis=0)
|
| 507 |
+
styler_mi.hide(styler_mi.columns[1:], axis=1)
|
| 508 |
+
styler_mi.map_index(lambda v: "color: red;", axis=0)
|
| 509 |
+
styler_mi.map_index(lambda v: "color: green;", axis=1)
|
| 510 |
+
styler_mi.map(lambda v: "color: blue;")
|
| 511 |
+
expected = dedent(
|
| 512 |
+
"""\
|
| 513 |
+
<style type="text/css">
|
| 514 |
+
#T__ROW0_col0 {
|
| 515 |
+
color: blue;
|
| 516 |
+
}
|
| 517 |
+
#T__LEVEL0_ROW0, #T__LEVEL1_ROW0 {
|
| 518 |
+
color: red;
|
| 519 |
+
}
|
| 520 |
+
#T__LEVEL0_col0, #T__LEVEL1_col0 {
|
| 521 |
+
color: green;
|
| 522 |
+
}
|
| 523 |
+
</style>
|
| 524 |
+
<table id="T_">
|
| 525 |
+
<thead>
|
| 526 |
+
<tr>
|
| 527 |
+
<th class="BLANK" > </th>
|
| 528 |
+
<th class="IDXNAME LEVEL0" >n1</th>
|
| 529 |
+
<th id="T__LEVEL0_col0" class="col_heading LEVEL0 col0" >a</th>
|
| 530 |
+
</tr>
|
| 531 |
+
<tr>
|
| 532 |
+
<th class="BLANK" > </th>
|
| 533 |
+
<th class="IDXNAME LEVEL1" >n2</th>
|
| 534 |
+
<th id="T__LEVEL1_col0" class="col_heading LEVEL1 col0" >c</th>
|
| 535 |
+
</tr>
|
| 536 |
+
<tr>
|
| 537 |
+
<th class="IDXNAME LEVEL0" >n1</th>
|
| 538 |
+
<th class="IDXNAME LEVEL1" >n2</th>
|
| 539 |
+
<th class="BLANK col0" > </th>
|
| 540 |
+
</tr>
|
| 541 |
+
</thead>
|
| 542 |
+
<tbody>
|
| 543 |
+
<tr>
|
| 544 |
+
<th id="T__LEVEL0_ROW0" class="ROWHEAD LEVEL0 ROW0" >a</th>
|
| 545 |
+
<th id="T__LEVEL1_ROW0" class="ROWHEAD LEVEL1 ROW0" >c</th>
|
| 546 |
+
<td id="T__ROW0_col0" class="DATA ROW0 col0" >0</td>
|
| 547 |
+
</tr>
|
| 548 |
+
</tbody>
|
| 549 |
+
</table>
|
| 550 |
+
"""
|
| 551 |
+
)
|
| 552 |
+
result = styler_mi.to_html()
|
| 553 |
+
assert result == expected
|
| 554 |
+
|
| 555 |
+
|
| 556 |
+
def test_include_css_style_rules_only_for_visible_cells(styler_mi):
|
| 557 |
+
# GH 43619
|
| 558 |
+
result = (
|
| 559 |
+
styler_mi.set_uuid("")
|
| 560 |
+
.map(lambda v: "color: blue;")
|
| 561 |
+
.hide(styler_mi.data.columns[1:], axis="columns")
|
| 562 |
+
.hide(styler_mi.data.index[1:], axis="index")
|
| 563 |
+
.to_html()
|
| 564 |
+
)
|
| 565 |
+
expected_styles = dedent(
|
| 566 |
+
"""\
|
| 567 |
+
<style type="text/css">
|
| 568 |
+
#T__row0_col0 {
|
| 569 |
+
color: blue;
|
| 570 |
+
}
|
| 571 |
+
</style>
|
| 572 |
+
"""
|
| 573 |
+
)
|
| 574 |
+
assert expected_styles in result
|
| 575 |
+
|
| 576 |
+
|
| 577 |
+
def test_include_css_style_rules_only_for_visible_index_labels(styler_mi):
|
| 578 |
+
# GH 43619
|
| 579 |
+
result = (
|
| 580 |
+
styler_mi.set_uuid("")
|
| 581 |
+
.map_index(lambda v: "color: blue;", axis="index")
|
| 582 |
+
.hide(styler_mi.data.columns, axis="columns")
|
| 583 |
+
.hide(styler_mi.data.index[1:], axis="index")
|
| 584 |
+
.to_html()
|
| 585 |
+
)
|
| 586 |
+
expected_styles = dedent(
|
| 587 |
+
"""\
|
| 588 |
+
<style type="text/css">
|
| 589 |
+
#T__level0_row0, #T__level1_row0 {
|
| 590 |
+
color: blue;
|
| 591 |
+
}
|
| 592 |
+
</style>
|
| 593 |
+
"""
|
| 594 |
+
)
|
| 595 |
+
assert expected_styles in result
|
| 596 |
+
|
| 597 |
+
|
| 598 |
+
def test_include_css_style_rules_only_for_visible_column_labels(styler_mi):
|
| 599 |
+
# GH 43619
|
| 600 |
+
result = (
|
| 601 |
+
styler_mi.set_uuid("")
|
| 602 |
+
.map_index(lambda v: "color: blue;", axis="columns")
|
| 603 |
+
.hide(styler_mi.data.columns[1:], axis="columns")
|
| 604 |
+
.hide(styler_mi.data.index, axis="index")
|
| 605 |
+
.to_html()
|
| 606 |
+
)
|
| 607 |
+
expected_styles = dedent(
|
| 608 |
+
"""\
|
| 609 |
+
<style type="text/css">
|
| 610 |
+
#T__level0_col0, #T__level1_col0 {
|
| 611 |
+
color: blue;
|
| 612 |
+
}
|
| 613 |
+
</style>
|
| 614 |
+
"""
|
| 615 |
+
)
|
| 616 |
+
assert expected_styles in result
|
| 617 |
+
|
| 618 |
+
|
| 619 |
+
def test_hiding_index_columns_multiindex_alignment():
|
| 620 |
+
# gh 43644
|
| 621 |
+
midx = MultiIndex.from_product(
|
| 622 |
+
[["i0", "j0"], ["i1"], ["i2", "j2"]], names=["i-0", "i-1", "i-2"]
|
| 623 |
+
)
|
| 624 |
+
cidx = MultiIndex.from_product(
|
| 625 |
+
[["c0"], ["c1", "d1"], ["c2", "d2"]], names=["c-0", "c-1", "c-2"]
|
| 626 |
+
)
|
| 627 |
+
df = DataFrame(np.arange(16).reshape(4, 4), index=midx, columns=cidx)
|
| 628 |
+
styler = Styler(df, uuid_len=0)
|
| 629 |
+
styler.hide(level=1, axis=0).hide(level=0, axis=1)
|
| 630 |
+
styler.hide([("j0", "i1", "j2")], axis=0)
|
| 631 |
+
styler.hide([("c0", "d1", "d2")], axis=1)
|
| 632 |
+
result = styler.to_html()
|
| 633 |
+
expected = dedent(
|
| 634 |
+
"""\
|
| 635 |
+
<style type="text/css">
|
| 636 |
+
</style>
|
| 637 |
+
<table id="T_">
|
| 638 |
+
<thead>
|
| 639 |
+
<tr>
|
| 640 |
+
<th class="blank" > </th>
|
| 641 |
+
<th class="index_name level1" >c-1</th>
|
| 642 |
+
<th id="T__level1_col0" class="col_heading level1 col0" colspan="2">c1</th>
|
| 643 |
+
<th id="T__level1_col2" class="col_heading level1 col2" >d1</th>
|
| 644 |
+
</tr>
|
| 645 |
+
<tr>
|
| 646 |
+
<th class="blank" > </th>
|
| 647 |
+
<th class="index_name level2" >c-2</th>
|
| 648 |
+
<th id="T__level2_col0" class="col_heading level2 col0" >c2</th>
|
| 649 |
+
<th id="T__level2_col1" class="col_heading level2 col1" >d2</th>
|
| 650 |
+
<th id="T__level2_col2" class="col_heading level2 col2" >c2</th>
|
| 651 |
+
</tr>
|
| 652 |
+
<tr>
|
| 653 |
+
<th class="index_name level0" >i-0</th>
|
| 654 |
+
<th class="index_name level2" >i-2</th>
|
| 655 |
+
<th class="blank col0" > </th>
|
| 656 |
+
<th class="blank col1" > </th>
|
| 657 |
+
<th class="blank col2" > </th>
|
| 658 |
+
</tr>
|
| 659 |
+
</thead>
|
| 660 |
+
<tbody>
|
| 661 |
+
<tr>
|
| 662 |
+
<th id="T__level0_row0" class="row_heading level0 row0" rowspan="2">i0</th>
|
| 663 |
+
<th id="T__level2_row0" class="row_heading level2 row0" >i2</th>
|
| 664 |
+
<td id="T__row0_col0" class="data row0 col0" >0</td>
|
| 665 |
+
<td id="T__row0_col1" class="data row0 col1" >1</td>
|
| 666 |
+
<td id="T__row0_col2" class="data row0 col2" >2</td>
|
| 667 |
+
</tr>
|
| 668 |
+
<tr>
|
| 669 |
+
<th id="T__level2_row1" class="row_heading level2 row1" >j2</th>
|
| 670 |
+
<td id="T__row1_col0" class="data row1 col0" >4</td>
|
| 671 |
+
<td id="T__row1_col1" class="data row1 col1" >5</td>
|
| 672 |
+
<td id="T__row1_col2" class="data row1 col2" >6</td>
|
| 673 |
+
</tr>
|
| 674 |
+
<tr>
|
| 675 |
+
<th id="T__level0_row2" class="row_heading level0 row2" >j0</th>
|
| 676 |
+
<th id="T__level2_row2" class="row_heading level2 row2" >i2</th>
|
| 677 |
+
<td id="T__row2_col0" class="data row2 col0" >8</td>
|
| 678 |
+
<td id="T__row2_col1" class="data row2 col1" >9</td>
|
| 679 |
+
<td id="T__row2_col2" class="data row2 col2" >10</td>
|
| 680 |
+
</tr>
|
| 681 |
+
</tbody>
|
| 682 |
+
</table>
|
| 683 |
+
"""
|
| 684 |
+
)
|
| 685 |
+
assert result == expected
|
| 686 |
+
|
| 687 |
+
|
| 688 |
+
def test_hiding_index_columns_multiindex_trimming():
|
| 689 |
+
# gh 44272
|
| 690 |
+
df = DataFrame(np.arange(64).reshape(8, 8))
|
| 691 |
+
df.columns = MultiIndex.from_product([[0, 1, 2, 3], [0, 1]])
|
| 692 |
+
df.index = MultiIndex.from_product([[0, 1, 2, 3], [0, 1]])
|
| 693 |
+
df.index.names, df.columns.names = ["a", "b"], ["c", "d"]
|
| 694 |
+
styler = Styler(df, cell_ids=False, uuid_len=0)
|
| 695 |
+
styler.hide([(0, 0), (0, 1), (1, 0)], axis=1).hide([(0, 0), (0, 1), (1, 0)], axis=0)
|
| 696 |
+
with option_context("styler.render.max_rows", 4, "styler.render.max_columns", 4):
|
| 697 |
+
result = styler.to_html()
|
| 698 |
+
|
| 699 |
+
expected = dedent(
|
| 700 |
+
"""\
|
| 701 |
+
<style type="text/css">
|
| 702 |
+
</style>
|
| 703 |
+
<table id="T_">
|
| 704 |
+
<thead>
|
| 705 |
+
<tr>
|
| 706 |
+
<th class="blank" > </th>
|
| 707 |
+
<th class="index_name level0" >c</th>
|
| 708 |
+
<th class="col_heading level0 col3" >1</th>
|
| 709 |
+
<th class="col_heading level0 col4" colspan="2">2</th>
|
| 710 |
+
<th class="col_heading level0 col6" >3</th>
|
| 711 |
+
</tr>
|
| 712 |
+
<tr>
|
| 713 |
+
<th class="blank" > </th>
|
| 714 |
+
<th class="index_name level1" >d</th>
|
| 715 |
+
<th class="col_heading level1 col3" >1</th>
|
| 716 |
+
<th class="col_heading level1 col4" >0</th>
|
| 717 |
+
<th class="col_heading level1 col5" >1</th>
|
| 718 |
+
<th class="col_heading level1 col6" >0</th>
|
| 719 |
+
<th class="col_heading level1 col_trim" >...</th>
|
| 720 |
+
</tr>
|
| 721 |
+
<tr>
|
| 722 |
+
<th class="index_name level0" >a</th>
|
| 723 |
+
<th class="index_name level1" >b</th>
|
| 724 |
+
<th class="blank col3" > </th>
|
| 725 |
+
<th class="blank col4" > </th>
|
| 726 |
+
<th class="blank col5" > </th>
|
| 727 |
+
<th class="blank col6" > </th>
|
| 728 |
+
<th class="blank col7 col_trim" > </th>
|
| 729 |
+
</tr>
|
| 730 |
+
</thead>
|
| 731 |
+
<tbody>
|
| 732 |
+
<tr>
|
| 733 |
+
<th class="row_heading level0 row3" >1</th>
|
| 734 |
+
<th class="row_heading level1 row3" >1</th>
|
| 735 |
+
<td class="data row3 col3" >27</td>
|
| 736 |
+
<td class="data row3 col4" >28</td>
|
| 737 |
+
<td class="data row3 col5" >29</td>
|
| 738 |
+
<td class="data row3 col6" >30</td>
|
| 739 |
+
<td class="data row3 col_trim" >...</td>
|
| 740 |
+
</tr>
|
| 741 |
+
<tr>
|
| 742 |
+
<th class="row_heading level0 row4" rowspan="2">2</th>
|
| 743 |
+
<th class="row_heading level1 row4" >0</th>
|
| 744 |
+
<td class="data row4 col3" >35</td>
|
| 745 |
+
<td class="data row4 col4" >36</td>
|
| 746 |
+
<td class="data row4 col5" >37</td>
|
| 747 |
+
<td class="data row4 col6" >38</td>
|
| 748 |
+
<td class="data row4 col_trim" >...</td>
|
| 749 |
+
</tr>
|
| 750 |
+
<tr>
|
| 751 |
+
<th class="row_heading level1 row5" >1</th>
|
| 752 |
+
<td class="data row5 col3" >43</td>
|
| 753 |
+
<td class="data row5 col4" >44</td>
|
| 754 |
+
<td class="data row5 col5" >45</td>
|
| 755 |
+
<td class="data row5 col6" >46</td>
|
| 756 |
+
<td class="data row5 col_trim" >...</td>
|
| 757 |
+
</tr>
|
| 758 |
+
<tr>
|
| 759 |
+
<th class="row_heading level0 row6" >3</th>
|
| 760 |
+
<th class="row_heading level1 row6" >0</th>
|
| 761 |
+
<td class="data row6 col3" >51</td>
|
| 762 |
+
<td class="data row6 col4" >52</td>
|
| 763 |
+
<td class="data row6 col5" >53</td>
|
| 764 |
+
<td class="data row6 col6" >54</td>
|
| 765 |
+
<td class="data row6 col_trim" >...</td>
|
| 766 |
+
</tr>
|
| 767 |
+
<tr>
|
| 768 |
+
<th class="row_heading level0 row_trim" >...</th>
|
| 769 |
+
<th class="row_heading level1 row_trim" >...</th>
|
| 770 |
+
<td class="data col3 row_trim" >...</td>
|
| 771 |
+
<td class="data col4 row_trim" >...</td>
|
| 772 |
+
<td class="data col5 row_trim" >...</td>
|
| 773 |
+
<td class="data col6 row_trim" >...</td>
|
| 774 |
+
<td class="data row_trim col_trim" >...</td>
|
| 775 |
+
</tr>
|
| 776 |
+
</tbody>
|
| 777 |
+
</table>
|
| 778 |
+
"""
|
| 779 |
+
)
|
| 780 |
+
|
| 781 |
+
assert result == expected
|
| 782 |
+
|
| 783 |
+
|
| 784 |
+
@pytest.mark.parametrize("type", ["data", "index"])
|
| 785 |
+
@pytest.mark.parametrize(
|
| 786 |
+
"text, exp, found",
|
| 787 |
+
[
|
| 788 |
+
("no link, just text", False, ""),
|
| 789 |
+
("subdomain not www: sub.web.com", False, ""),
|
| 790 |
+
("www subdomain: www.web.com other", True, "www.web.com"),
|
| 791 |
+
("scheme full structure: http://www.web.com", True, "http://www.web.com"),
|
| 792 |
+
("scheme no top-level: http://www.web", True, "http://www.web"),
|
| 793 |
+
("no scheme, no top-level: www.web", False, "www.web"),
|
| 794 |
+
("https scheme: https://www.web.com", True, "https://www.web.com"),
|
| 795 |
+
("ftp scheme: ftp://www.web", True, "ftp://www.web"),
|
| 796 |
+
("ftps scheme: ftps://www.web", True, "ftps://www.web"),
|
| 797 |
+
("subdirectories: www.web.com/directory", True, "www.web.com/directory"),
|
| 798 |
+
("Multiple domains: www.1.2.3.4", True, "www.1.2.3.4"),
|
| 799 |
+
("with port: http://web.com:80", True, "http://web.com:80"),
|
| 800 |
+
(
|
| 801 |
+
"full net_loc scheme: http://user:pass@web.com",
|
| 802 |
+
True,
|
| 803 |
+
"http://user:pass@web.com",
|
| 804 |
+
),
|
| 805 |
+
(
|
| 806 |
+
"with valid special chars: http://web.com/,.':;~!@#$*()[]",
|
| 807 |
+
True,
|
| 808 |
+
"http://web.com/,.':;~!@#$*()[]",
|
| 809 |
+
),
|
| 810 |
+
],
|
| 811 |
+
)
|
| 812 |
+
def test_rendered_links(type, text, exp, found):
|
| 813 |
+
if type == "data":
|
| 814 |
+
df = DataFrame([text])
|
| 815 |
+
styler = df.style.format(hyperlinks="html")
|
| 816 |
+
else:
|
| 817 |
+
df = DataFrame([0], index=[text])
|
| 818 |
+
styler = df.style.format_index(hyperlinks="html")
|
| 819 |
+
|
| 820 |
+
rendered = f'<a href="{found}" target="_blank">{found}</a>'
|
| 821 |
+
result = styler.to_html()
|
| 822 |
+
assert (rendered in result) is exp
|
| 823 |
+
assert (text in result) is not exp # test conversion done when expected and not
|
| 824 |
+
|
| 825 |
+
|
| 826 |
+
def test_multiple_rendered_links():
|
| 827 |
+
links = ("www.a.b", "http://a.c", "https://a.d", "ftp://a.e")
|
| 828 |
+
df = DataFrame(["text {} {} text {} {}".format(*links)])
|
| 829 |
+
result = df.style.format(hyperlinks="html").to_html()
|
| 830 |
+
href = '<a href="{0}" target="_blank">{0}</a>'
|
| 831 |
+
for link in links:
|
| 832 |
+
assert href.format(link) in result
|
| 833 |
+
assert href.format("text") not in result
|
| 834 |
+
|
| 835 |
+
|
| 836 |
+
def test_concat(styler):
|
| 837 |
+
other = styler.data.agg(["mean"]).style
|
| 838 |
+
styler.concat(other).set_uuid("X")
|
| 839 |
+
result = styler.to_html()
|
| 840 |
+
fp = "foot0_"
|
| 841 |
+
expected = dedent(
|
| 842 |
+
f"""\
|
| 843 |
+
<tr>
|
| 844 |
+
<th id="T_X_level0_row1" class="row_heading level0 row1" >b</th>
|
| 845 |
+
<td id="T_X_row1_col0" class="data row1 col0" >2.690000</td>
|
| 846 |
+
</tr>
|
| 847 |
+
<tr>
|
| 848 |
+
<th id="T_X_level0_{fp}row0" class="{fp}row_heading level0 {fp}row0" >mean</th>
|
| 849 |
+
<td id="T_X_{fp}row0_col0" class="{fp}data {fp}row0 col0" >2.650000</td>
|
| 850 |
+
</tr>
|
| 851 |
+
</tbody>
|
| 852 |
+
</table>
|
| 853 |
+
"""
|
| 854 |
+
)
|
| 855 |
+
assert expected in result
|
| 856 |
+
|
| 857 |
+
|
| 858 |
+
def test_concat_recursion(styler):
|
| 859 |
+
df = styler.data
|
| 860 |
+
styler1 = styler
|
| 861 |
+
styler2 = Styler(df.agg(["mean"]), precision=3)
|
| 862 |
+
styler3 = Styler(df.agg(["mean"]), precision=4)
|
| 863 |
+
styler1.concat(styler2.concat(styler3)).set_uuid("X")
|
| 864 |
+
result = styler.to_html()
|
| 865 |
+
# notice that the second concat (last <tr> of the output html),
|
| 866 |
+
# there are two `foot_` in the id and class
|
| 867 |
+
fp1 = "foot0_"
|
| 868 |
+
fp2 = "foot0_foot0_"
|
| 869 |
+
expected = dedent(
|
| 870 |
+
f"""\
|
| 871 |
+
<tr>
|
| 872 |
+
<th id="T_X_level0_row1" class="row_heading level0 row1" >b</th>
|
| 873 |
+
<td id="T_X_row1_col0" class="data row1 col0" >2.690000</td>
|
| 874 |
+
</tr>
|
| 875 |
+
<tr>
|
| 876 |
+
<th id="T_X_level0_{fp1}row0" class="{fp1}row_heading level0 {fp1}row0" >mean</th>
|
| 877 |
+
<td id="T_X_{fp1}row0_col0" class="{fp1}data {fp1}row0 col0" >2.650</td>
|
| 878 |
+
</tr>
|
| 879 |
+
<tr>
|
| 880 |
+
<th id="T_X_level0_{fp2}row0" class="{fp2}row_heading level0 {fp2}row0" >mean</th>
|
| 881 |
+
<td id="T_X_{fp2}row0_col0" class="{fp2}data {fp2}row0 col0" >2.6500</td>
|
| 882 |
+
</tr>
|
| 883 |
+
</tbody>
|
| 884 |
+
</table>
|
| 885 |
+
"""
|
| 886 |
+
)
|
| 887 |
+
assert expected in result
|
| 888 |
+
|
| 889 |
+
|
| 890 |
+
def test_concat_chain(styler):
|
| 891 |
+
df = styler.data
|
| 892 |
+
styler1 = styler
|
| 893 |
+
styler2 = Styler(df.agg(["mean"]), precision=3)
|
| 894 |
+
styler3 = Styler(df.agg(["mean"]), precision=4)
|
| 895 |
+
styler1.concat(styler2).concat(styler3).set_uuid("X")
|
| 896 |
+
result = styler.to_html()
|
| 897 |
+
fp1 = "foot0_"
|
| 898 |
+
fp2 = "foot1_"
|
| 899 |
+
expected = dedent(
|
| 900 |
+
f"""\
|
| 901 |
+
<tr>
|
| 902 |
+
<th id="T_X_level0_row1" class="row_heading level0 row1" >b</th>
|
| 903 |
+
<td id="T_X_row1_col0" class="data row1 col0" >2.690000</td>
|
| 904 |
+
</tr>
|
| 905 |
+
<tr>
|
| 906 |
+
<th id="T_X_level0_{fp1}row0" class="{fp1}row_heading level0 {fp1}row0" >mean</th>
|
| 907 |
+
<td id="T_X_{fp1}row0_col0" class="{fp1}data {fp1}row0 col0" >2.650</td>
|
| 908 |
+
</tr>
|
| 909 |
+
<tr>
|
| 910 |
+
<th id="T_X_level0_{fp2}row0" class="{fp2}row_heading level0 {fp2}row0" >mean</th>
|
| 911 |
+
<td id="T_X_{fp2}row0_col0" class="{fp2}data {fp2}row0 col0" >2.6500</td>
|
| 912 |
+
</tr>
|
| 913 |
+
</tbody>
|
| 914 |
+
</table>
|
| 915 |
+
"""
|
| 916 |
+
)
|
| 917 |
+
assert expected in result
|
| 918 |
+
|
| 919 |
+
|
| 920 |
+
def test_concat_combined():
|
| 921 |
+
def html_lines(foot_prefix: str):
|
| 922 |
+
assert foot_prefix.endswith("_") or foot_prefix == ""
|
| 923 |
+
fp = foot_prefix
|
| 924 |
+
return indent(
|
| 925 |
+
dedent(
|
| 926 |
+
f"""\
|
| 927 |
+
<tr>
|
| 928 |
+
<th id="T_X_level0_{fp}row0" class="{fp}row_heading level0 {fp}row0" >a</th>
|
| 929 |
+
<td id="T_X_{fp}row0_col0" class="{fp}data {fp}row0 col0" >2.610000</td>
|
| 930 |
+
</tr>
|
| 931 |
+
<tr>
|
| 932 |
+
<th id="T_X_level0_{fp}row1" class="{fp}row_heading level0 {fp}row1" >b</th>
|
| 933 |
+
<td id="T_X_{fp}row1_col0" class="{fp}data {fp}row1 col0" >2.690000</td>
|
| 934 |
+
</tr>
|
| 935 |
+
"""
|
| 936 |
+
),
|
| 937 |
+
prefix=" " * 4,
|
| 938 |
+
)
|
| 939 |
+
|
| 940 |
+
df = DataFrame([[2.61], [2.69]], index=["a", "b"], columns=["A"])
|
| 941 |
+
s1 = df.style.highlight_max(color="red")
|
| 942 |
+
s2 = df.style.highlight_max(color="green")
|
| 943 |
+
s3 = df.style.highlight_max(color="blue")
|
| 944 |
+
s4 = df.style.highlight_max(color="yellow")
|
| 945 |
+
|
| 946 |
+
result = s1.concat(s2).concat(s3.concat(s4)).set_uuid("X").to_html()
|
| 947 |
+
expected_css = dedent(
|
| 948 |
+
"""\
|
| 949 |
+
<style type="text/css">
|
| 950 |
+
#T_X_row1_col0 {
|
| 951 |
+
background-color: red;
|
| 952 |
+
}
|
| 953 |
+
#T_X_foot0_row1_col0 {
|
| 954 |
+
background-color: green;
|
| 955 |
+
}
|
| 956 |
+
#T_X_foot1_row1_col0 {
|
| 957 |
+
background-color: blue;
|
| 958 |
+
}
|
| 959 |
+
#T_X_foot1_foot0_row1_col0 {
|
| 960 |
+
background-color: yellow;
|
| 961 |
+
}
|
| 962 |
+
</style>
|
| 963 |
+
"""
|
| 964 |
+
)
|
| 965 |
+
expected_table = (
|
| 966 |
+
dedent(
|
| 967 |
+
"""\
|
| 968 |
+
<table id="T_X">
|
| 969 |
+
<thead>
|
| 970 |
+
<tr>
|
| 971 |
+
<th class="blank level0" > </th>
|
| 972 |
+
<th id="T_X_level0_col0" class="col_heading level0 col0" >A</th>
|
| 973 |
+
</tr>
|
| 974 |
+
</thead>
|
| 975 |
+
<tbody>
|
| 976 |
+
"""
|
| 977 |
+
)
|
| 978 |
+
+ html_lines("")
|
| 979 |
+
+ html_lines("foot0_")
|
| 980 |
+
+ html_lines("foot1_")
|
| 981 |
+
+ html_lines("foot1_foot0_")
|
| 982 |
+
+ dedent(
|
| 983 |
+
"""\
|
| 984 |
+
</tbody>
|
| 985 |
+
</table>
|
| 986 |
+
"""
|
| 987 |
+
)
|
| 988 |
+
)
|
| 989 |
+
assert expected_css + expected_table == result
|
| 990 |
+
|
| 991 |
+
|
| 992 |
+
def test_to_html_na_rep_non_scalar_data(datapath):
|
| 993 |
+
# GH47103
|
| 994 |
+
df = DataFrame([{"a": 1, "b": [1, 2, 3], "c": np.nan}])
|
| 995 |
+
result = df.style.format(na_rep="-").to_html(table_uuid="test")
|
| 996 |
+
expected = """\
|
| 997 |
+
<style type="text/css">
|
| 998 |
+
</style>
|
| 999 |
+
<table id="T_test">
|
| 1000 |
+
<thead>
|
| 1001 |
+
<tr>
|
| 1002 |
+
<th class="blank level0" > </th>
|
| 1003 |
+
<th id="T_test_level0_col0" class="col_heading level0 col0" >a</th>
|
| 1004 |
+
<th id="T_test_level0_col1" class="col_heading level0 col1" >b</th>
|
| 1005 |
+
<th id="T_test_level0_col2" class="col_heading level0 col2" >c</th>
|
| 1006 |
+
</tr>
|
| 1007 |
+
</thead>
|
| 1008 |
+
<tbody>
|
| 1009 |
+
<tr>
|
| 1010 |
+
<th id="T_test_level0_row0" class="row_heading level0 row0" >0</th>
|
| 1011 |
+
<td id="T_test_row0_col0" class="data row0 col0" >1</td>
|
| 1012 |
+
<td id="T_test_row0_col1" class="data row0 col1" >[1, 2, 3]</td>
|
| 1013 |
+
<td id="T_test_row0_col2" class="data row0 col2" >-</td>
|
| 1014 |
+
</tr>
|
| 1015 |
+
</tbody>
|
| 1016 |
+
</table>
|
| 1017 |
+
"""
|
| 1018 |
+
assert result == expected
|
| 1019 |
+
|
| 1020 |
+
|
| 1021 |
+
@pytest.mark.parametrize("escape_axis_0", [True, False])
|
| 1022 |
+
@pytest.mark.parametrize("escape_axis_1", [True, False])
|
| 1023 |
+
def test_format_index_names(styler_multi, escape_axis_0, escape_axis_1):
|
| 1024 |
+
if escape_axis_0:
|
| 1025 |
+
styler_multi.format_index_names(axis=0, escape="html")
|
| 1026 |
+
expected_index = ["X>", "y_"]
|
| 1027 |
+
else:
|
| 1028 |
+
expected_index = ["X>", "y_"]
|
| 1029 |
+
|
| 1030 |
+
if escape_axis_1:
|
| 1031 |
+
styler_multi.format_index_names(axis=1, escape="html")
|
| 1032 |
+
expected_columns = ["A&", "b&"]
|
| 1033 |
+
else:
|
| 1034 |
+
expected_columns = ["A&", "b&"]
|
| 1035 |
+
|
| 1036 |
+
result = styler_multi.to_html(table_uuid="test")
|
| 1037 |
+
for expected_str in expected_index + expected_columns:
|
| 1038 |
+
assert f"{expected_str}</th>" in result
|
.venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_matplotlib.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pytest
|
| 3 |
+
|
| 4 |
+
from pandas import (
|
| 5 |
+
DataFrame,
|
| 6 |
+
IndexSlice,
|
| 7 |
+
Series,
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
mpl = pytest.importorskip("matplotlib")
|
| 11 |
+
pytest.importorskip("jinja2")
|
| 12 |
+
|
| 13 |
+
from pandas.io.formats.style import Styler
|
| 14 |
+
|
| 15 |
+
pytestmark = pytest.mark.usefixtures("mpl_cleanup")
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@pytest.fixture
|
| 19 |
+
def df():
|
| 20 |
+
return DataFrame([[1, 2], [2, 4]], columns=["A", "B"])
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@pytest.fixture
|
| 24 |
+
def styler(df):
|
| 25 |
+
return Styler(df, uuid_len=0)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@pytest.fixture
|
| 29 |
+
def df_blank():
|
| 30 |
+
return DataFrame([[0, 0], [0, 0]], columns=["A", "B"], index=["X", "Y"])
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@pytest.fixture
|
| 34 |
+
def styler_blank(df_blank):
|
| 35 |
+
return Styler(df_blank, uuid_len=0)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@pytest.mark.parametrize("f", ["background_gradient", "text_gradient"])
|
| 39 |
+
def test_function_gradient(styler, f):
|
| 40 |
+
for c_map in [None, "YlOrRd"]:
|
| 41 |
+
result = getattr(styler, f)(cmap=c_map)._compute().ctx
|
| 42 |
+
assert all("#" in x[0][1] for x in result.values())
|
| 43 |
+
assert result[(0, 0)] == result[(0, 1)]
|
| 44 |
+
assert result[(1, 0)] == result[(1, 1)]
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@pytest.mark.parametrize("f", ["background_gradient", "text_gradient"])
|
| 48 |
+
def test_background_gradient_color(styler, f):
|
| 49 |
+
result = getattr(styler, f)(subset=IndexSlice[1, "A"])._compute().ctx
|
| 50 |
+
if f == "background_gradient":
|
| 51 |
+
assert result[(1, 0)] == [("background-color", "#fff7fb"), ("color", "#000000")]
|
| 52 |
+
elif f == "text_gradient":
|
| 53 |
+
assert result[(1, 0)] == [("color", "#fff7fb")]
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@pytest.mark.parametrize(
|
| 57 |
+
"axis, expected",
|
| 58 |
+
[
|
| 59 |
+
(0, ["low", "low", "high", "high"]),
|
| 60 |
+
(1, ["low", "high", "low", "high"]),
|
| 61 |
+
(None, ["low", "mid", "mid", "high"]),
|
| 62 |
+
],
|
| 63 |
+
)
|
| 64 |
+
@pytest.mark.parametrize("f", ["background_gradient", "text_gradient"])
|
| 65 |
+
def test_background_gradient_axis(styler, axis, expected, f):
|
| 66 |
+
if f == "background_gradient":
|
| 67 |
+
colors = {
|
| 68 |
+
"low": [("background-color", "#f7fbff"), ("color", "#000000")],
|
| 69 |
+
"mid": [("background-color", "#abd0e6"), ("color", "#000000")],
|
| 70 |
+
"high": [("background-color", "#08306b"), ("color", "#f1f1f1")],
|
| 71 |
+
}
|
| 72 |
+
elif f == "text_gradient":
|
| 73 |
+
colors = {
|
| 74 |
+
"low": [("color", "#f7fbff")],
|
| 75 |
+
"mid": [("color", "#abd0e6")],
|
| 76 |
+
"high": [("color", "#08306b")],
|
| 77 |
+
}
|
| 78 |
+
result = getattr(styler, f)(cmap="Blues", axis=axis)._compute().ctx
|
| 79 |
+
for i, cell in enumerate([(0, 0), (0, 1), (1, 0), (1, 1)]):
|
| 80 |
+
assert result[cell] == colors[expected[i]]
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
@pytest.mark.parametrize(
|
| 84 |
+
"cmap, expected",
|
| 85 |
+
[
|
| 86 |
+
(
|
| 87 |
+
"PuBu",
|
| 88 |
+
{
|
| 89 |
+
(4, 5): [("background-color", "#86b0d3"), ("color", "#000000")],
|
| 90 |
+
(4, 6): [("background-color", "#83afd3"), ("color", "#f1f1f1")],
|
| 91 |
+
},
|
| 92 |
+
),
|
| 93 |
+
(
|
| 94 |
+
"YlOrRd",
|
| 95 |
+
{
|
| 96 |
+
(4, 8): [("background-color", "#fd913e"), ("color", "#000000")],
|
| 97 |
+
(4, 9): [("background-color", "#fd8f3d"), ("color", "#f1f1f1")],
|
| 98 |
+
},
|
| 99 |
+
),
|
| 100 |
+
(
|
| 101 |
+
None,
|
| 102 |
+
{
|
| 103 |
+
(7, 0): [("background-color", "#48c16e"), ("color", "#f1f1f1")],
|
| 104 |
+
(7, 1): [("background-color", "#4cc26c"), ("color", "#000000")],
|
| 105 |
+
},
|
| 106 |
+
),
|
| 107 |
+
],
|
| 108 |
+
)
|
| 109 |
+
def test_text_color_threshold(cmap, expected):
|
| 110 |
+
# GH 39888
|
| 111 |
+
df = DataFrame(np.arange(100).reshape(10, 10))
|
| 112 |
+
result = df.style.background_gradient(cmap=cmap, axis=None)._compute().ctx
|
| 113 |
+
for k in expected.keys():
|
| 114 |
+
assert result[k] == expected[k]
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def test_background_gradient_vmin_vmax():
|
| 118 |
+
# GH 12145
|
| 119 |
+
df = DataFrame(range(5))
|
| 120 |
+
ctx = df.style.background_gradient(vmin=1, vmax=3)._compute().ctx
|
| 121 |
+
assert ctx[(0, 0)] == ctx[(1, 0)]
|
| 122 |
+
assert ctx[(4, 0)] == ctx[(3, 0)]
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def test_background_gradient_int64():
|
| 126 |
+
# GH 28869
|
| 127 |
+
df1 = Series(range(3)).to_frame()
|
| 128 |
+
df2 = Series(range(3), dtype="Int64").to_frame()
|
| 129 |
+
ctx1 = df1.style.background_gradient()._compute().ctx
|
| 130 |
+
ctx2 = df2.style.background_gradient()._compute().ctx
|
| 131 |
+
assert ctx2[(0, 0)] == ctx1[(0, 0)]
|
| 132 |
+
assert ctx2[(1, 0)] == ctx1[(1, 0)]
|
| 133 |
+
assert ctx2[(2, 0)] == ctx1[(2, 0)]
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
@pytest.mark.parametrize(
|
| 137 |
+
"axis, gmap, expected",
|
| 138 |
+
[
|
| 139 |
+
(
|
| 140 |
+
0,
|
| 141 |
+
[1, 2],
|
| 142 |
+
{
|
| 143 |
+
(0, 0): [("background-color", "#fff7fb"), ("color", "#000000")],
|
| 144 |
+
(1, 0): [("background-color", "#023858"), ("color", "#f1f1f1")],
|
| 145 |
+
(0, 1): [("background-color", "#fff7fb"), ("color", "#000000")],
|
| 146 |
+
(1, 1): [("background-color", "#023858"), ("color", "#f1f1f1")],
|
| 147 |
+
},
|
| 148 |
+
),
|
| 149 |
+
(
|
| 150 |
+
1,
|
| 151 |
+
[1, 2],
|
| 152 |
+
{
|
| 153 |
+
(0, 0): [("background-color", "#fff7fb"), ("color", "#000000")],
|
| 154 |
+
(1, 0): [("background-color", "#fff7fb"), ("color", "#000000")],
|
| 155 |
+
(0, 1): [("background-color", "#023858"), ("color", "#f1f1f1")],
|
| 156 |
+
(1, 1): [("background-color", "#023858"), ("color", "#f1f1f1")],
|
| 157 |
+
},
|
| 158 |
+
),
|
| 159 |
+
(
|
| 160 |
+
None,
|
| 161 |
+
np.array([[2, 1], [1, 2]]),
|
| 162 |
+
{
|
| 163 |
+
(0, 0): [("background-color", "#023858"), ("color", "#f1f1f1")],
|
| 164 |
+
(1, 0): [("background-color", "#fff7fb"), ("color", "#000000")],
|
| 165 |
+
(0, 1): [("background-color", "#fff7fb"), ("color", "#000000")],
|
| 166 |
+
(1, 1): [("background-color", "#023858"), ("color", "#f1f1f1")],
|
| 167 |
+
},
|
| 168 |
+
),
|
| 169 |
+
],
|
| 170 |
+
)
|
| 171 |
+
def test_background_gradient_gmap_array(styler_blank, axis, gmap, expected):
|
| 172 |
+
# tests when gmap is given as a sequence and converted to ndarray
|
| 173 |
+
result = styler_blank.background_gradient(axis=axis, gmap=gmap)._compute().ctx
|
| 174 |
+
assert result == expected
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
@pytest.mark.parametrize(
|
| 178 |
+
"gmap, axis", [([1, 2, 3], 0), ([1, 2], 1), (np.array([[1, 2], [1, 2]]), None)]
|
| 179 |
+
)
|
| 180 |
+
def test_background_gradient_gmap_array_raises(gmap, axis):
|
| 181 |
+
# test when gmap as converted ndarray is bad shape
|
| 182 |
+
df = DataFrame([[0, 0, 0], [0, 0, 0]])
|
| 183 |
+
msg = "supplied 'gmap' is not correct shape"
|
| 184 |
+
with pytest.raises(ValueError, match=msg):
|
| 185 |
+
df.style.background_gradient(gmap=gmap, axis=axis)._compute()
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
@pytest.mark.parametrize(
|
| 189 |
+
"gmap",
|
| 190 |
+
[
|
| 191 |
+
DataFrame( # reverse the columns
|
| 192 |
+
[[2, 1], [1, 2]], columns=["B", "A"], index=["X", "Y"]
|
| 193 |
+
),
|
| 194 |
+
DataFrame( # reverse the index
|
| 195 |
+
[[2, 1], [1, 2]], columns=["A", "B"], index=["Y", "X"]
|
| 196 |
+
),
|
| 197 |
+
DataFrame( # reverse the index and columns
|
| 198 |
+
[[1, 2], [2, 1]], columns=["B", "A"], index=["Y", "X"]
|
| 199 |
+
),
|
| 200 |
+
DataFrame( # add unnecessary columns
|
| 201 |
+
[[1, 2, 3], [2, 1, 3]], columns=["A", "B", "C"], index=["X", "Y"]
|
| 202 |
+
),
|
| 203 |
+
DataFrame( # add unnecessary index
|
| 204 |
+
[[1, 2], [2, 1], [3, 3]], columns=["A", "B"], index=["X", "Y", "Z"]
|
| 205 |
+
),
|
| 206 |
+
],
|
| 207 |
+
)
|
| 208 |
+
@pytest.mark.parametrize(
|
| 209 |
+
"subset, exp_gmap", # exp_gmap is underlying map DataFrame should conform to
|
| 210 |
+
[
|
| 211 |
+
(None, [[1, 2], [2, 1]]),
|
| 212 |
+
(["A"], [[1], [2]]), # slice only column "A" in data and gmap
|
| 213 |
+
(["B", "A"], [[2, 1], [1, 2]]), # reverse the columns in data
|
| 214 |
+
(IndexSlice["X", :], [[1, 2]]), # slice only index "X" in data and gmap
|
| 215 |
+
(IndexSlice[["Y", "X"], :], [[2, 1], [1, 2]]), # reverse the index in data
|
| 216 |
+
],
|
| 217 |
+
)
|
| 218 |
+
def test_background_gradient_gmap_dataframe_align(styler_blank, gmap, subset, exp_gmap):
|
| 219 |
+
# test gmap given as DataFrame that it aligns to the data including subset
|
| 220 |
+
expected = styler_blank.background_gradient(axis=None, gmap=exp_gmap, subset=subset)
|
| 221 |
+
result = styler_blank.background_gradient(axis=None, gmap=gmap, subset=subset)
|
| 222 |
+
assert expected._compute().ctx == result._compute().ctx
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
@pytest.mark.parametrize(
|
| 226 |
+
"gmap, axis, exp_gmap",
|
| 227 |
+
[
|
| 228 |
+
(Series([2, 1], index=["Y", "X"]), 0, [[1, 1], [2, 2]]), # reverse the index
|
| 229 |
+
(Series([2, 1], index=["B", "A"]), 1, [[1, 2], [1, 2]]), # reverse the cols
|
| 230 |
+
(Series([1, 2, 3], index=["X", "Y", "Z"]), 0, [[1, 1], [2, 2]]), # add idx
|
| 231 |
+
(Series([1, 2, 3], index=["A", "B", "C"]), 1, [[1, 2], [1, 2]]), # add col
|
| 232 |
+
],
|
| 233 |
+
)
|
| 234 |
+
def test_background_gradient_gmap_series_align(styler_blank, gmap, axis, exp_gmap):
|
| 235 |
+
# test gmap given as Series that it aligns to the data including subset
|
| 236 |
+
expected = styler_blank.background_gradient(axis=None, gmap=exp_gmap)._compute()
|
| 237 |
+
result = styler_blank.background_gradient(axis=axis, gmap=gmap)._compute()
|
| 238 |
+
assert expected.ctx == result.ctx
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
@pytest.mark.parametrize("axis", [1, 0])
|
| 242 |
+
def test_background_gradient_gmap_wrong_dataframe(styler_blank, axis):
|
| 243 |
+
# test giving a gmap in DataFrame but with wrong axis
|
| 244 |
+
gmap = DataFrame([[1, 2], [2, 1]], columns=["A", "B"], index=["X", "Y"])
|
| 245 |
+
msg = "'gmap' is a DataFrame but underlying data for operations is a Series"
|
| 246 |
+
with pytest.raises(ValueError, match=msg):
|
| 247 |
+
styler_blank.background_gradient(gmap=gmap, axis=axis)._compute()
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def test_background_gradient_gmap_wrong_series(styler_blank):
|
| 251 |
+
# test giving a gmap in Series form but with wrong axis
|
| 252 |
+
msg = "'gmap' is a Series but underlying data for operations is a DataFrame"
|
| 253 |
+
gmap = Series([1, 2], index=["X", "Y"])
|
| 254 |
+
with pytest.raises(ValueError, match=msg):
|
| 255 |
+
styler_blank.background_gradient(gmap=gmap, axis=None)._compute()
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def test_background_gradient_nullable_dtypes():
|
| 259 |
+
# GH 50712
|
| 260 |
+
df1 = DataFrame([[1], [0], [np.nan]], dtype=float)
|
| 261 |
+
df2 = DataFrame([[1], [0], [None]], dtype="Int64")
|
| 262 |
+
|
| 263 |
+
ctx1 = df1.style.background_gradient()._compute().ctx
|
| 264 |
+
ctx2 = df2.style.background_gradient()._compute().ctx
|
| 265 |
+
assert ctx1 == ctx2
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
@pytest.mark.parametrize(
|
| 269 |
+
"cmap",
|
| 270 |
+
["PuBu", mpl.colormaps["PuBu"]],
|
| 271 |
+
)
|
| 272 |
+
def test_bar_colormap(cmap):
|
| 273 |
+
data = DataFrame([[1, 2], [3, 4]])
|
| 274 |
+
ctx = data.style.bar(cmap=cmap, axis=None)._compute().ctx
|
| 275 |
+
pubu_colors = {
|
| 276 |
+
(0, 0): "#d0d1e6",
|
| 277 |
+
(1, 0): "#056faf",
|
| 278 |
+
(0, 1): "#73a9cf",
|
| 279 |
+
(1, 1): "#023858",
|
| 280 |
+
}
|
| 281 |
+
for k, v in pubu_colors.items():
|
| 282 |
+
assert v in ctx[k][1][1]
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
def test_bar_color_raises(df):
|
| 286 |
+
msg = "`color` must be string or list or tuple of 2 strings"
|
| 287 |
+
with pytest.raises(ValueError, match=msg):
|
| 288 |
+
df.style.bar(color={"a", "b"}).to_html()
|
| 289 |
+
with pytest.raises(ValueError, match=msg):
|
| 290 |
+
df.style.bar(color=["a", "b", "c"]).to_html()
|
| 291 |
+
|
| 292 |
+
msg = "`color` and `cmap` cannot both be given"
|
| 293 |
+
with pytest.raises(ValueError, match=msg):
|
| 294 |
+
df.style.bar(color="something", cmap="something else").to_html()
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
@pytest.mark.parametrize("plot_method", ["scatter", "hexbin"])
|
| 298 |
+
def test_pass_colormap_instance(df, plot_method):
|
| 299 |
+
# https://github.com/pandas-dev/pandas/issues/49374
|
| 300 |
+
cmap = mpl.colors.ListedColormap([[1, 1, 1], [0, 0, 0]])
|
| 301 |
+
df["c"] = df.A + df.B
|
| 302 |
+
kwargs = {"x": "A", "y": "B", "c": "c", "colormap": cmap}
|
| 303 |
+
if plot_method == "hexbin":
|
| 304 |
+
kwargs["C"] = kwargs.pop("c")
|
| 305 |
+
getattr(df.plot, plot_method)(**kwargs)
|
.venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_non_unique.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from textwrap import dedent
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
from pandas import (
|
| 6 |
+
DataFrame,
|
| 7 |
+
IndexSlice,
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
pytest.importorskip("jinja2")
|
| 11 |
+
|
| 12 |
+
from pandas.io.formats.style import Styler
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@pytest.fixture
|
| 16 |
+
def df():
|
| 17 |
+
return DataFrame(
|
| 18 |
+
[[1, 2, 3], [4, 5, 6], [7, 8, 9]],
|
| 19 |
+
index=["i", "j", "j"],
|
| 20 |
+
columns=["c", "d", "d"],
|
| 21 |
+
dtype=float,
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@pytest.fixture
|
| 26 |
+
def styler(df):
|
| 27 |
+
return Styler(df, uuid_len=0)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def test_format_non_unique(df):
|
| 31 |
+
# GH 41269
|
| 32 |
+
|
| 33 |
+
# test dict
|
| 34 |
+
html = df.style.format({"d": "{:.1f}"}).to_html()
|
| 35 |
+
for val in ["1.000000<", "4.000000<", "7.000000<"]:
|
| 36 |
+
assert val in html
|
| 37 |
+
for val in ["2.0<", "3.0<", "5.0<", "6.0<", "8.0<", "9.0<"]:
|
| 38 |
+
assert val in html
|
| 39 |
+
|
| 40 |
+
# test subset
|
| 41 |
+
html = df.style.format(precision=1, subset=IndexSlice["j", "d"]).to_html()
|
| 42 |
+
for val in ["1.000000<", "4.000000<", "7.000000<", "2.000000<", "3.000000<"]:
|
| 43 |
+
assert val in html
|
| 44 |
+
for val in ["5.0<", "6.0<", "8.0<", "9.0<"]:
|
| 45 |
+
assert val in html
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@pytest.mark.parametrize("func", ["apply", "map"])
|
| 49 |
+
def test_apply_map_non_unique_raises(df, func):
|
| 50 |
+
# GH 41269
|
| 51 |
+
if func == "apply":
|
| 52 |
+
op = lambda s: ["color: red;"] * len(s)
|
| 53 |
+
else:
|
| 54 |
+
op = lambda v: "color: red;"
|
| 55 |
+
|
| 56 |
+
with pytest.raises(KeyError, match="`Styler.apply` and `.map` are not"):
|
| 57 |
+
getattr(df.style, func)(op)._compute()
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def test_table_styles_dict_non_unique_index(styler):
|
| 61 |
+
styles = styler.set_table_styles(
|
| 62 |
+
{"j": [{"selector": "td", "props": "a: v;"}]}, axis=1
|
| 63 |
+
).table_styles
|
| 64 |
+
assert styles == [
|
| 65 |
+
{"selector": "td.row1", "props": [("a", "v")]},
|
| 66 |
+
{"selector": "td.row2", "props": [("a", "v")]},
|
| 67 |
+
]
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def test_table_styles_dict_non_unique_columns(styler):
|
| 71 |
+
styles = styler.set_table_styles(
|
| 72 |
+
{"d": [{"selector": "td", "props": "a: v;"}]}, axis=0
|
| 73 |
+
).table_styles
|
| 74 |
+
assert styles == [
|
| 75 |
+
{"selector": "td.col1", "props": [("a", "v")]},
|
| 76 |
+
{"selector": "td.col2", "props": [("a", "v")]},
|
| 77 |
+
]
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def test_tooltips_non_unique_raises(styler):
|
| 81 |
+
# ttips has unique keys
|
| 82 |
+
ttips = DataFrame([["1", "2"], ["3", "4"]], columns=["c", "d"], index=["a", "b"])
|
| 83 |
+
styler.set_tooltips(ttips=ttips) # OK
|
| 84 |
+
|
| 85 |
+
# ttips has non-unique columns
|
| 86 |
+
ttips = DataFrame([["1", "2"], ["3", "4"]], columns=["c", "c"], index=["a", "b"])
|
| 87 |
+
with pytest.raises(KeyError, match="Tooltips render only if `ttips` has unique"):
|
| 88 |
+
styler.set_tooltips(ttips=ttips)
|
| 89 |
+
|
| 90 |
+
# ttips has non-unique index
|
| 91 |
+
ttips = DataFrame([["1", "2"], ["3", "4"]], columns=["c", "d"], index=["a", "a"])
|
| 92 |
+
with pytest.raises(KeyError, match="Tooltips render only if `ttips` has unique"):
|
| 93 |
+
styler.set_tooltips(ttips=ttips)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def test_set_td_classes_non_unique_raises(styler):
|
| 97 |
+
# classes has unique keys
|
| 98 |
+
classes = DataFrame([["1", "2"], ["3", "4"]], columns=["c", "d"], index=["a", "b"])
|
| 99 |
+
styler.set_td_classes(classes=classes) # OK
|
| 100 |
+
|
| 101 |
+
# classes has non-unique columns
|
| 102 |
+
classes = DataFrame([["1", "2"], ["3", "4"]], columns=["c", "c"], index=["a", "b"])
|
| 103 |
+
with pytest.raises(KeyError, match="Classes render only if `classes` has unique"):
|
| 104 |
+
styler.set_td_classes(classes=classes)
|
| 105 |
+
|
| 106 |
+
# classes has non-unique index
|
| 107 |
+
classes = DataFrame([["1", "2"], ["3", "4"]], columns=["c", "d"], index=["a", "a"])
|
| 108 |
+
with pytest.raises(KeyError, match="Classes render only if `classes` has unique"):
|
| 109 |
+
styler.set_td_classes(classes=classes)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def test_hide_columns_non_unique(styler):
|
| 113 |
+
ctx = styler.hide(["d"], axis="columns")._translate(True, True)
|
| 114 |
+
|
| 115 |
+
assert ctx["head"][0][1]["display_value"] == "c"
|
| 116 |
+
assert ctx["head"][0][1]["is_visible"] is True
|
| 117 |
+
|
| 118 |
+
assert ctx["head"][0][2]["display_value"] == "d"
|
| 119 |
+
assert ctx["head"][0][2]["is_visible"] is False
|
| 120 |
+
|
| 121 |
+
assert ctx["head"][0][3]["display_value"] == "d"
|
| 122 |
+
assert ctx["head"][0][3]["is_visible"] is False
|
| 123 |
+
|
| 124 |
+
assert ctx["body"][0][1]["is_visible"] is True
|
| 125 |
+
assert ctx["body"][0][2]["is_visible"] is False
|
| 126 |
+
assert ctx["body"][0][3]["is_visible"] is False
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def test_latex_non_unique(styler):
|
| 130 |
+
result = styler.to_latex()
|
| 131 |
+
assert result == dedent(
|
| 132 |
+
"""\
|
| 133 |
+
\\begin{tabular}{lrrr}
|
| 134 |
+
& c & d & d \\\\
|
| 135 |
+
i & 1.000000 & 2.000000 & 3.000000 \\\\
|
| 136 |
+
j & 4.000000 & 5.000000 & 6.000000 \\\\
|
| 137 |
+
j & 7.000000 & 8.000000 & 9.000000 \\\\
|
| 138 |
+
\\end{tabular}
|
| 139 |
+
"""
|
| 140 |
+
)
|
.venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_style.py
ADDED
|
@@ -0,0 +1,1602 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import contextlib
|
| 2 |
+
import copy
|
| 3 |
+
import re
|
| 4 |
+
from textwrap import dedent
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
import pytest
|
| 8 |
+
|
| 9 |
+
from pandas import (
|
| 10 |
+
DataFrame,
|
| 11 |
+
IndexSlice,
|
| 12 |
+
MultiIndex,
|
| 13 |
+
Series,
|
| 14 |
+
option_context,
|
| 15 |
+
)
|
| 16 |
+
import pandas._testing as tm
|
| 17 |
+
|
| 18 |
+
jinja2 = pytest.importorskip("jinja2")
|
| 19 |
+
from pandas.io.formats.style import ( # isort:skip
|
| 20 |
+
Styler,
|
| 21 |
+
)
|
| 22 |
+
from pandas.io.formats.style_render import (
|
| 23 |
+
_get_level_lengths,
|
| 24 |
+
_get_trimming_maximums,
|
| 25 |
+
maybe_convert_css_to_tuples,
|
| 26 |
+
non_reducing_slice,
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@pytest.fixture
|
| 31 |
+
def mi_df():
|
| 32 |
+
return DataFrame(
|
| 33 |
+
[[1, 2], [3, 4]],
|
| 34 |
+
index=MultiIndex.from_product([["i0"], ["i1_a", "i1_b"]]),
|
| 35 |
+
columns=MultiIndex.from_product([["c0"], ["c1_a", "c1_b"]]),
|
| 36 |
+
dtype=int,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@pytest.fixture
|
| 41 |
+
def mi_styler(mi_df):
|
| 42 |
+
return Styler(mi_df, uuid_len=0)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@pytest.fixture
|
| 46 |
+
def mi_styler_comp(mi_styler):
|
| 47 |
+
# comprehensively add features to mi_styler
|
| 48 |
+
mi_styler = mi_styler._copy(deepcopy=True)
|
| 49 |
+
mi_styler.css = {**mi_styler.css, "row": "ROW", "col": "COL"}
|
| 50 |
+
mi_styler.uuid_len = 5
|
| 51 |
+
mi_styler.uuid = "abcde"
|
| 52 |
+
mi_styler.set_caption("capt")
|
| 53 |
+
mi_styler.set_table_styles([{"selector": "a", "props": "a:v;"}])
|
| 54 |
+
mi_styler.hide(axis="columns")
|
| 55 |
+
mi_styler.hide([("c0", "c1_a")], axis="columns", names=True)
|
| 56 |
+
mi_styler.hide(axis="index")
|
| 57 |
+
mi_styler.hide([("i0", "i1_a")], axis="index", names=True)
|
| 58 |
+
mi_styler.set_table_attributes('class="box"')
|
| 59 |
+
other = mi_styler.data.agg(["mean"])
|
| 60 |
+
other.index = MultiIndex.from_product([[""], other.index])
|
| 61 |
+
mi_styler.concat(other.style)
|
| 62 |
+
mi_styler.format(na_rep="MISSING", precision=3)
|
| 63 |
+
mi_styler.format_index(precision=2, axis=0)
|
| 64 |
+
mi_styler.format_index(precision=4, axis=1)
|
| 65 |
+
mi_styler.highlight_max(axis=None)
|
| 66 |
+
mi_styler.map_index(lambda x: "color: white;", axis=0)
|
| 67 |
+
mi_styler.map_index(lambda x: "color: black;", axis=1)
|
| 68 |
+
mi_styler.set_td_classes(
|
| 69 |
+
DataFrame(
|
| 70 |
+
[["a", "b"], ["a", "c"]], index=mi_styler.index, columns=mi_styler.columns
|
| 71 |
+
)
|
| 72 |
+
)
|
| 73 |
+
mi_styler.set_tooltips(
|
| 74 |
+
DataFrame(
|
| 75 |
+
[["a2", "b2"], ["a2", "c2"]],
|
| 76 |
+
index=mi_styler.index,
|
| 77 |
+
columns=mi_styler.columns,
|
| 78 |
+
)
|
| 79 |
+
)
|
| 80 |
+
mi_styler.format_index_names(escape="html", axis=0)
|
| 81 |
+
mi_styler.format_index_names(escape="html", axis=1)
|
| 82 |
+
return mi_styler
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
@pytest.fixture
|
| 86 |
+
def blank_value():
|
| 87 |
+
return " "
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
@pytest.fixture
|
| 91 |
+
def df():
|
| 92 |
+
df = DataFrame({"A": [0, 1], "B": np.random.default_rng(2).standard_normal(2)})
|
| 93 |
+
return df
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
@pytest.fixture
|
| 97 |
+
def styler(df):
|
| 98 |
+
df = DataFrame({"A": [0, 1], "B": np.random.default_rng(2).standard_normal(2)})
|
| 99 |
+
return Styler(df)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
@pytest.mark.parametrize(
|
| 103 |
+
"sparse_columns, exp_cols",
|
| 104 |
+
[
|
| 105 |
+
(
|
| 106 |
+
True,
|
| 107 |
+
[
|
| 108 |
+
{"is_visible": True, "attributes": 'colspan="2"', "value": "c0"},
|
| 109 |
+
{"is_visible": False, "attributes": "", "value": "c0"},
|
| 110 |
+
],
|
| 111 |
+
),
|
| 112 |
+
(
|
| 113 |
+
False,
|
| 114 |
+
[
|
| 115 |
+
{"is_visible": True, "attributes": "", "value": "c0"},
|
| 116 |
+
{"is_visible": True, "attributes": "", "value": "c0"},
|
| 117 |
+
],
|
| 118 |
+
),
|
| 119 |
+
],
|
| 120 |
+
)
|
| 121 |
+
def test_mi_styler_sparsify_columns(mi_styler, sparse_columns, exp_cols):
|
| 122 |
+
exp_l1_c0 = {"is_visible": True, "attributes": "", "display_value": "c1_a"}
|
| 123 |
+
exp_l1_c1 = {"is_visible": True, "attributes": "", "display_value": "c1_b"}
|
| 124 |
+
|
| 125 |
+
ctx = mi_styler._translate(True, sparse_columns)
|
| 126 |
+
|
| 127 |
+
assert exp_cols[0].items() <= ctx["head"][0][2].items()
|
| 128 |
+
assert exp_cols[1].items() <= ctx["head"][0][3].items()
|
| 129 |
+
assert exp_l1_c0.items() <= ctx["head"][1][2].items()
|
| 130 |
+
assert exp_l1_c1.items() <= ctx["head"][1][3].items()
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
@pytest.mark.parametrize(
|
| 134 |
+
"sparse_index, exp_rows",
|
| 135 |
+
[
|
| 136 |
+
(
|
| 137 |
+
True,
|
| 138 |
+
[
|
| 139 |
+
{"is_visible": True, "attributes": 'rowspan="2"', "value": "i0"},
|
| 140 |
+
{"is_visible": False, "attributes": "", "value": "i0"},
|
| 141 |
+
],
|
| 142 |
+
),
|
| 143 |
+
(
|
| 144 |
+
False,
|
| 145 |
+
[
|
| 146 |
+
{"is_visible": True, "attributes": "", "value": "i0"},
|
| 147 |
+
{"is_visible": True, "attributes": "", "value": "i0"},
|
| 148 |
+
],
|
| 149 |
+
),
|
| 150 |
+
],
|
| 151 |
+
)
|
| 152 |
+
def test_mi_styler_sparsify_index(mi_styler, sparse_index, exp_rows):
|
| 153 |
+
exp_l1_r0 = {"is_visible": True, "attributes": "", "display_value": "i1_a"}
|
| 154 |
+
exp_l1_r1 = {"is_visible": True, "attributes": "", "display_value": "i1_b"}
|
| 155 |
+
|
| 156 |
+
ctx = mi_styler._translate(sparse_index, True)
|
| 157 |
+
|
| 158 |
+
assert exp_rows[0].items() <= ctx["body"][0][0].items()
|
| 159 |
+
assert exp_rows[1].items() <= ctx["body"][1][0].items()
|
| 160 |
+
assert exp_l1_r0.items() <= ctx["body"][0][1].items()
|
| 161 |
+
assert exp_l1_r1.items() <= ctx["body"][1][1].items()
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def test_mi_styler_sparsify_options(mi_styler):
|
| 165 |
+
with option_context("styler.sparse.index", False):
|
| 166 |
+
html1 = mi_styler.to_html()
|
| 167 |
+
with option_context("styler.sparse.index", True):
|
| 168 |
+
html2 = mi_styler.to_html()
|
| 169 |
+
|
| 170 |
+
assert html1 != html2
|
| 171 |
+
|
| 172 |
+
with option_context("styler.sparse.columns", False):
|
| 173 |
+
html1 = mi_styler.to_html()
|
| 174 |
+
with option_context("styler.sparse.columns", True):
|
| 175 |
+
html2 = mi_styler.to_html()
|
| 176 |
+
|
| 177 |
+
assert html1 != html2
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
@pytest.mark.parametrize(
|
| 181 |
+
"rn, cn, max_els, max_rows, max_cols, exp_rn, exp_cn",
|
| 182 |
+
[
|
| 183 |
+
(100, 100, 100, None, None, 12, 6), # reduce to (12, 6) < 100 elements
|
| 184 |
+
(1000, 3, 750, None, None, 250, 3), # dynamically reduce rows to 250, keep cols
|
| 185 |
+
(4, 1000, 500, None, None, 4, 125), # dynamically reduce cols to 125, keep rows
|
| 186 |
+
(1000, 3, 750, 10, None, 10, 3), # overwrite above dynamics with max_row
|
| 187 |
+
(4, 1000, 500, None, 5, 4, 5), # overwrite above dynamics with max_col
|
| 188 |
+
(100, 100, 700, 50, 50, 25, 25), # rows cols below given maxes so < 700 elmts
|
| 189 |
+
],
|
| 190 |
+
)
|
| 191 |
+
def test_trimming_maximum(rn, cn, max_els, max_rows, max_cols, exp_rn, exp_cn):
|
| 192 |
+
rn, cn = _get_trimming_maximums(
|
| 193 |
+
rn, cn, max_els, max_rows, max_cols, scaling_factor=0.5
|
| 194 |
+
)
|
| 195 |
+
assert (rn, cn) == (exp_rn, exp_cn)
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
@pytest.mark.parametrize(
|
| 199 |
+
"option, val",
|
| 200 |
+
[
|
| 201 |
+
("styler.render.max_elements", 6),
|
| 202 |
+
("styler.render.max_rows", 3),
|
| 203 |
+
],
|
| 204 |
+
)
|
| 205 |
+
def test_render_trimming_rows(option, val):
|
| 206 |
+
# test auto and specific trimming of rows
|
| 207 |
+
df = DataFrame(np.arange(120).reshape(60, 2))
|
| 208 |
+
with option_context(option, val):
|
| 209 |
+
ctx = df.style._translate(True, True)
|
| 210 |
+
assert len(ctx["head"][0]) == 3 # index + 2 data cols
|
| 211 |
+
assert len(ctx["body"]) == 4 # 3 data rows + trimming row
|
| 212 |
+
assert len(ctx["body"][0]) == 3 # index + 2 data cols
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
@pytest.mark.parametrize(
|
| 216 |
+
"option, val",
|
| 217 |
+
[
|
| 218 |
+
("styler.render.max_elements", 6),
|
| 219 |
+
("styler.render.max_columns", 2),
|
| 220 |
+
],
|
| 221 |
+
)
|
| 222 |
+
def test_render_trimming_cols(option, val):
|
| 223 |
+
# test auto and specific trimming of cols
|
| 224 |
+
df = DataFrame(np.arange(30).reshape(3, 10))
|
| 225 |
+
with option_context(option, val):
|
| 226 |
+
ctx = df.style._translate(True, True)
|
| 227 |
+
assert len(ctx["head"][0]) == 4 # index + 2 data cols + trimming col
|
| 228 |
+
assert len(ctx["body"]) == 3 # 3 data rows
|
| 229 |
+
assert len(ctx["body"][0]) == 4 # index + 2 data cols + trimming col
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def test_render_trimming_mi():
|
| 233 |
+
midx = MultiIndex.from_product([[1, 2], [1, 2, 3]])
|
| 234 |
+
df = DataFrame(np.arange(36).reshape(6, 6), columns=midx, index=midx)
|
| 235 |
+
with option_context("styler.render.max_elements", 4):
|
| 236 |
+
ctx = df.style._translate(True, True)
|
| 237 |
+
|
| 238 |
+
assert len(ctx["body"][0]) == 5 # 2 indexes + 2 data cols + trimming row
|
| 239 |
+
assert {"attributes": 'rowspan="2"'}.items() <= ctx["body"][0][0].items()
|
| 240 |
+
assert {"class": "data row0 col_trim"}.items() <= ctx["body"][0][4].items()
|
| 241 |
+
assert {"class": "data row_trim col_trim"}.items() <= ctx["body"][2][4].items()
|
| 242 |
+
assert len(ctx["body"]) == 3 # 2 data rows + trimming row
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def test_render_empty_mi():
|
| 246 |
+
# GH 43305
|
| 247 |
+
df = DataFrame(index=MultiIndex.from_product([["A"], [0, 1]], names=[None, "one"]))
|
| 248 |
+
expected = dedent(
|
| 249 |
+
"""\
|
| 250 |
+
>
|
| 251 |
+
<thead>
|
| 252 |
+
<tr>
|
| 253 |
+
<th class="index_name level0" > </th>
|
| 254 |
+
<th class="index_name level1" >one</th>
|
| 255 |
+
</tr>
|
| 256 |
+
</thead>
|
| 257 |
+
"""
|
| 258 |
+
)
|
| 259 |
+
assert expected in df.style.to_html()
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
@pytest.mark.parametrize("comprehensive", [True, False])
|
| 263 |
+
@pytest.mark.parametrize("render", [True, False])
|
| 264 |
+
@pytest.mark.parametrize("deepcopy", [True, False])
|
| 265 |
+
def test_copy(comprehensive, render, deepcopy, mi_styler, mi_styler_comp):
|
| 266 |
+
styler = mi_styler_comp if comprehensive else mi_styler
|
| 267 |
+
styler.uuid_len = 5
|
| 268 |
+
|
| 269 |
+
s2 = copy.deepcopy(styler) if deepcopy else copy.copy(styler) # make copy and check
|
| 270 |
+
assert s2 is not styler
|
| 271 |
+
|
| 272 |
+
if render:
|
| 273 |
+
styler.to_html()
|
| 274 |
+
|
| 275 |
+
excl = [
|
| 276 |
+
"cellstyle_map", # render time vars..
|
| 277 |
+
"cellstyle_map_columns",
|
| 278 |
+
"cellstyle_map_index",
|
| 279 |
+
"template_latex", # render templates are class level
|
| 280 |
+
"template_html",
|
| 281 |
+
"template_html_style",
|
| 282 |
+
"template_html_table",
|
| 283 |
+
]
|
| 284 |
+
if not deepcopy: # check memory locations are equal for all included attributes
|
| 285 |
+
for attr in [a for a in styler.__dict__ if (not callable(a) and a not in excl)]:
|
| 286 |
+
assert id(getattr(s2, attr)) == id(getattr(styler, attr))
|
| 287 |
+
else: # check memory locations are different for nested or mutable vars
|
| 288 |
+
shallow = [
|
| 289 |
+
"data",
|
| 290 |
+
"columns",
|
| 291 |
+
"index",
|
| 292 |
+
"uuid_len",
|
| 293 |
+
"uuid",
|
| 294 |
+
"caption",
|
| 295 |
+
"cell_ids",
|
| 296 |
+
"hide_index_",
|
| 297 |
+
"hide_columns_",
|
| 298 |
+
"hide_index_names",
|
| 299 |
+
"hide_column_names",
|
| 300 |
+
"table_attributes",
|
| 301 |
+
]
|
| 302 |
+
for attr in shallow:
|
| 303 |
+
assert id(getattr(s2, attr)) == id(getattr(styler, attr))
|
| 304 |
+
|
| 305 |
+
for attr in [
|
| 306 |
+
a
|
| 307 |
+
for a in styler.__dict__
|
| 308 |
+
if (not callable(a) and a not in excl and a not in shallow)
|
| 309 |
+
]:
|
| 310 |
+
if getattr(s2, attr) is None:
|
| 311 |
+
assert id(getattr(s2, attr)) == id(getattr(styler, attr))
|
| 312 |
+
else:
|
| 313 |
+
assert id(getattr(s2, attr)) != id(getattr(styler, attr))
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
@pytest.mark.parametrize("deepcopy", [True, False])
|
| 317 |
+
def test_inherited_copy(mi_styler, deepcopy):
|
| 318 |
+
# Ensure that the inherited class is preserved when a Styler object is copied.
|
| 319 |
+
# GH 52728
|
| 320 |
+
class CustomStyler(Styler):
|
| 321 |
+
pass
|
| 322 |
+
|
| 323 |
+
custom_styler = CustomStyler(mi_styler.data)
|
| 324 |
+
custom_styler_copy = (
|
| 325 |
+
copy.deepcopy(custom_styler) if deepcopy else copy.copy(custom_styler)
|
| 326 |
+
)
|
| 327 |
+
assert isinstance(custom_styler_copy, CustomStyler)
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
def test_clear(mi_styler_comp):
|
| 331 |
+
# NOTE: if this test fails for new features then 'mi_styler_comp' should be updated
|
| 332 |
+
# to ensure proper testing of the 'copy', 'clear', 'export' methods with new feature
|
| 333 |
+
# GH 40675
|
| 334 |
+
styler = mi_styler_comp
|
| 335 |
+
styler._compute() # execute applied methods
|
| 336 |
+
|
| 337 |
+
clean_copy = Styler(styler.data, uuid=styler.uuid)
|
| 338 |
+
|
| 339 |
+
excl = [
|
| 340 |
+
"data",
|
| 341 |
+
"index",
|
| 342 |
+
"columns",
|
| 343 |
+
"uuid",
|
| 344 |
+
"uuid_len", # uuid is set to be the same on styler and clean_copy
|
| 345 |
+
"cell_ids",
|
| 346 |
+
"cellstyle_map", # execution time only
|
| 347 |
+
"cellstyle_map_columns", # execution time only
|
| 348 |
+
"cellstyle_map_index", # execution time only
|
| 349 |
+
"template_latex", # render templates are class level
|
| 350 |
+
"template_html",
|
| 351 |
+
"template_html_style",
|
| 352 |
+
"template_html_table",
|
| 353 |
+
]
|
| 354 |
+
# tests vars are not same vals on obj and clean copy before clear (except for excl)
|
| 355 |
+
for attr in [a for a in styler.__dict__ if not (callable(a) or a in excl)]:
|
| 356 |
+
res = getattr(styler, attr) == getattr(clean_copy, attr)
|
| 357 |
+
if hasattr(res, "__iter__") and len(res) > 0:
|
| 358 |
+
assert not all(res) # some element in iterable differs
|
| 359 |
+
elif hasattr(res, "__iter__") and len(res) == 0:
|
| 360 |
+
pass # empty array
|
| 361 |
+
else:
|
| 362 |
+
assert not res # explicit var differs
|
| 363 |
+
|
| 364 |
+
# test vars have same vales on obj and clean copy after clearing
|
| 365 |
+
styler.clear()
|
| 366 |
+
for attr in [a for a in styler.__dict__ if not callable(a)]:
|
| 367 |
+
res = getattr(styler, attr) == getattr(clean_copy, attr)
|
| 368 |
+
assert all(res) if hasattr(res, "__iter__") else res
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
def test_export(mi_styler_comp, mi_styler):
|
| 372 |
+
exp_attrs = [
|
| 373 |
+
"_todo",
|
| 374 |
+
"hide_index_",
|
| 375 |
+
"hide_index_names",
|
| 376 |
+
"hide_columns_",
|
| 377 |
+
"hide_column_names",
|
| 378 |
+
"table_attributes",
|
| 379 |
+
"table_styles",
|
| 380 |
+
"css",
|
| 381 |
+
]
|
| 382 |
+
for attr in exp_attrs:
|
| 383 |
+
check = getattr(mi_styler, attr) == getattr(mi_styler_comp, attr)
|
| 384 |
+
assert not (
|
| 385 |
+
all(check) if (hasattr(check, "__iter__") and len(check) > 0) else check
|
| 386 |
+
)
|
| 387 |
+
|
| 388 |
+
export = mi_styler_comp.export()
|
| 389 |
+
used = mi_styler.use(export)
|
| 390 |
+
for attr in exp_attrs:
|
| 391 |
+
check = getattr(used, attr) == getattr(mi_styler_comp, attr)
|
| 392 |
+
assert all(check) if (hasattr(check, "__iter__") and len(check) > 0) else check
|
| 393 |
+
|
| 394 |
+
used.to_html()
|
| 395 |
+
|
| 396 |
+
|
| 397 |
+
def test_hide_raises(mi_styler):
|
| 398 |
+
msg = "`subset` and `level` cannot be passed simultaneously"
|
| 399 |
+
with pytest.raises(ValueError, match=msg):
|
| 400 |
+
mi_styler.hide(axis="index", subset="something", level="something else")
|
| 401 |
+
|
| 402 |
+
msg = "`level` must be of type `int`, `str` or list of such"
|
| 403 |
+
with pytest.raises(ValueError, match=msg):
|
| 404 |
+
mi_styler.hide(axis="index", level={"bad": 1, "type": 2})
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
@pytest.mark.parametrize("level", [1, "one", [1], ["one"]])
|
| 408 |
+
def test_hide_index_level(mi_styler, level):
|
| 409 |
+
mi_styler.index.names, mi_styler.columns.names = ["zero", "one"], ["zero", "one"]
|
| 410 |
+
ctx = mi_styler.hide(axis="index", level=level)._translate(False, True)
|
| 411 |
+
assert len(ctx["head"][0]) == 3
|
| 412 |
+
assert len(ctx["head"][1]) == 3
|
| 413 |
+
assert len(ctx["head"][2]) == 4
|
| 414 |
+
assert ctx["head"][2][0]["is_visible"]
|
| 415 |
+
assert not ctx["head"][2][1]["is_visible"]
|
| 416 |
+
|
| 417 |
+
assert ctx["body"][0][0]["is_visible"]
|
| 418 |
+
assert not ctx["body"][0][1]["is_visible"]
|
| 419 |
+
assert ctx["body"][1][0]["is_visible"]
|
| 420 |
+
assert not ctx["body"][1][1]["is_visible"]
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
@pytest.mark.parametrize("level", [1, "one", [1], ["one"]])
|
| 424 |
+
@pytest.mark.parametrize("names", [True, False])
|
| 425 |
+
def test_hide_columns_level(mi_styler, level, names):
|
| 426 |
+
mi_styler.columns.names = ["zero", "one"]
|
| 427 |
+
if names:
|
| 428 |
+
mi_styler.index.names = ["zero", "one"]
|
| 429 |
+
ctx = mi_styler.hide(axis="columns", level=level)._translate(True, False)
|
| 430 |
+
assert len(ctx["head"]) == (2 if names else 1)
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
@pytest.mark.parametrize("method", ["map", "apply"])
|
| 434 |
+
@pytest.mark.parametrize("axis", ["index", "columns"])
|
| 435 |
+
def test_apply_map_header(method, axis):
|
| 436 |
+
# GH 41893
|
| 437 |
+
df = DataFrame({"A": [0, 0], "B": [1, 1]}, index=["C", "D"])
|
| 438 |
+
func = {
|
| 439 |
+
"apply": lambda s: ["attr: val" if ("A" in v or "C" in v) else "" for v in s],
|
| 440 |
+
"map": lambda v: "attr: val" if ("A" in v or "C" in v) else "",
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
# test execution added to todo
|
| 444 |
+
result = getattr(df.style, f"{method}_index")(func[method], axis=axis)
|
| 445 |
+
assert len(result._todo) == 1
|
| 446 |
+
assert len(getattr(result, f"ctx_{axis}")) == 0
|
| 447 |
+
|
| 448 |
+
# test ctx object on compute
|
| 449 |
+
result._compute()
|
| 450 |
+
expected = {
|
| 451 |
+
(0, 0): [("attr", "val")],
|
| 452 |
+
}
|
| 453 |
+
assert getattr(result, f"ctx_{axis}") == expected
|
| 454 |
+
|
| 455 |
+
|
| 456 |
+
@pytest.mark.parametrize("method", ["apply", "map"])
|
| 457 |
+
@pytest.mark.parametrize("axis", ["index", "columns"])
|
| 458 |
+
def test_apply_map_header_mi(mi_styler, method, axis):
|
| 459 |
+
# GH 41893
|
| 460 |
+
func = {
|
| 461 |
+
"apply": lambda s: ["attr: val;" if "b" in v else "" for v in s],
|
| 462 |
+
"map": lambda v: "attr: val" if "b" in v else "",
|
| 463 |
+
}
|
| 464 |
+
result = getattr(mi_styler, f"{method}_index")(func[method], axis=axis)._compute()
|
| 465 |
+
expected = {(1, 1): [("attr", "val")]}
|
| 466 |
+
assert getattr(result, f"ctx_{axis}") == expected
|
| 467 |
+
|
| 468 |
+
|
| 469 |
+
def test_apply_map_header_raises(mi_styler):
|
| 470 |
+
# GH 41893
|
| 471 |
+
with pytest.raises(ValueError, match="No axis named bad for object type DataFrame"):
|
| 472 |
+
mi_styler.map_index(lambda v: "attr: val;", axis="bad")._compute()
|
| 473 |
+
|
| 474 |
+
|
| 475 |
+
class TestStyler:
|
| 476 |
+
def test_init_non_pandas(self):
|
| 477 |
+
msg = "``data`` must be a Series or DataFrame"
|
| 478 |
+
with pytest.raises(TypeError, match=msg):
|
| 479 |
+
Styler([1, 2, 3])
|
| 480 |
+
|
| 481 |
+
def test_init_series(self):
|
| 482 |
+
result = Styler(Series([1, 2]))
|
| 483 |
+
assert result.data.ndim == 2
|
| 484 |
+
|
| 485 |
+
def test_repr_html_ok(self, styler):
|
| 486 |
+
styler._repr_html_()
|
| 487 |
+
|
| 488 |
+
def test_repr_html_mathjax(self, styler):
|
| 489 |
+
# gh-19824 / 41395
|
| 490 |
+
assert "tex2jax_ignore" not in styler._repr_html_()
|
| 491 |
+
assert "mathjax_ignore" not in styler._repr_html_()
|
| 492 |
+
|
| 493 |
+
with option_context("styler.html.mathjax", False):
|
| 494 |
+
assert "tex2jax_ignore" in styler._repr_html_()
|
| 495 |
+
assert "mathjax_ignore" in styler._repr_html_()
|
| 496 |
+
|
| 497 |
+
def test_update_ctx(self, styler):
|
| 498 |
+
styler._update_ctx(DataFrame({"A": ["color: red", "color: blue"]}))
|
| 499 |
+
expected = {(0, 0): [("color", "red")], (1, 0): [("color", "blue")]}
|
| 500 |
+
assert styler.ctx == expected
|
| 501 |
+
|
| 502 |
+
def test_update_ctx_flatten_multi_and_trailing_semi(self, styler):
|
| 503 |
+
attrs = DataFrame({"A": ["color: red; foo: bar", "color:blue ; foo: baz;"]})
|
| 504 |
+
styler._update_ctx(attrs)
|
| 505 |
+
expected = {
|
| 506 |
+
(0, 0): [("color", "red"), ("foo", "bar")],
|
| 507 |
+
(1, 0): [("color", "blue"), ("foo", "baz")],
|
| 508 |
+
}
|
| 509 |
+
assert styler.ctx == expected
|
| 510 |
+
|
| 511 |
+
def test_render(self):
|
| 512 |
+
df = DataFrame({"A": [0, 1]})
|
| 513 |
+
style = lambda x: Series(["color: red", "color: blue"], name=x.name)
|
| 514 |
+
s = Styler(df, uuid="AB").apply(style)
|
| 515 |
+
s.to_html()
|
| 516 |
+
# it worked?
|
| 517 |
+
|
| 518 |
+
def test_multiple_render(self, df):
|
| 519 |
+
# GH 39396
|
| 520 |
+
s = Styler(df, uuid_len=0).map(lambda x: "color: red;", subset=["A"])
|
| 521 |
+
s.to_html() # do 2 renders to ensure css styles not duplicated
|
| 522 |
+
assert (
|
| 523 |
+
'<style type="text/css">\n#T__row0_col0, #T__row1_col0 {\n'
|
| 524 |
+
" color: red;\n}\n</style>" in s.to_html()
|
| 525 |
+
)
|
| 526 |
+
|
| 527 |
+
def test_render_empty_dfs(self):
|
| 528 |
+
empty_df = DataFrame()
|
| 529 |
+
es = Styler(empty_df)
|
| 530 |
+
es.to_html()
|
| 531 |
+
# An index but no columns
|
| 532 |
+
DataFrame(columns=["a"]).style.to_html()
|
| 533 |
+
# A column but no index
|
| 534 |
+
DataFrame(index=["a"]).style.to_html()
|
| 535 |
+
# No IndexError raised?
|
| 536 |
+
|
| 537 |
+
def test_render_double(self):
|
| 538 |
+
df = DataFrame({"A": [0, 1]})
|
| 539 |
+
style = lambda x: Series(
|
| 540 |
+
["color: red; border: 1px", "color: blue; border: 2px"], name=x.name
|
| 541 |
+
)
|
| 542 |
+
s = Styler(df, uuid="AB").apply(style)
|
| 543 |
+
s.to_html()
|
| 544 |
+
# it worked?
|
| 545 |
+
|
| 546 |
+
def test_set_properties(self):
|
| 547 |
+
df = DataFrame({"A": [0, 1]})
|
| 548 |
+
result = df.style.set_properties(color="white", size="10px")._compute().ctx
|
| 549 |
+
# order is deterministic
|
| 550 |
+
v = [("color", "white"), ("size", "10px")]
|
| 551 |
+
expected = {(0, 0): v, (1, 0): v}
|
| 552 |
+
assert result.keys() == expected.keys()
|
| 553 |
+
for v1, v2 in zip(result.values(), expected.values()):
|
| 554 |
+
assert sorted(v1) == sorted(v2)
|
| 555 |
+
|
| 556 |
+
def test_set_properties_subset(self):
|
| 557 |
+
df = DataFrame({"A": [0, 1]})
|
| 558 |
+
result = (
|
| 559 |
+
df.style.set_properties(subset=IndexSlice[0, "A"], color="white")
|
| 560 |
+
._compute()
|
| 561 |
+
.ctx
|
| 562 |
+
)
|
| 563 |
+
expected = {(0, 0): [("color", "white")]}
|
| 564 |
+
assert result == expected
|
| 565 |
+
|
| 566 |
+
def test_empty_index_name_doesnt_display(self, blank_value):
|
| 567 |
+
# https://github.com/pandas-dev/pandas/pull/12090#issuecomment-180695902
|
| 568 |
+
df = DataFrame({"A": [1, 2], "B": [3, 4], "C": [5, 6]})
|
| 569 |
+
result = df.style._translate(True, True)
|
| 570 |
+
assert len(result["head"]) == 1
|
| 571 |
+
expected = {
|
| 572 |
+
"class": "blank level0",
|
| 573 |
+
"type": "th",
|
| 574 |
+
"value": blank_value,
|
| 575 |
+
"is_visible": True,
|
| 576 |
+
"display_value": blank_value,
|
| 577 |
+
}
|
| 578 |
+
assert expected.items() <= result["head"][0][0].items()
|
| 579 |
+
|
| 580 |
+
def test_index_name(self):
|
| 581 |
+
# https://github.com/pandas-dev/pandas/issues/11655
|
| 582 |
+
df = DataFrame({"A": [1, 2], "B": [3, 4], "C": [5, 6]})
|
| 583 |
+
result = df.set_index("A").style._translate(True, True)
|
| 584 |
+
expected = {
|
| 585 |
+
"class": "index_name level0",
|
| 586 |
+
"type": "th",
|
| 587 |
+
"value": "A",
|
| 588 |
+
"is_visible": True,
|
| 589 |
+
"display_value": "A",
|
| 590 |
+
}
|
| 591 |
+
assert expected.items() <= result["head"][1][0].items()
|
| 592 |
+
|
| 593 |
+
def test_numeric_columns(self):
|
| 594 |
+
# https://github.com/pandas-dev/pandas/issues/12125
|
| 595 |
+
# smoke test for _translate
|
| 596 |
+
df = DataFrame({0: [1, 2, 3]})
|
| 597 |
+
df.style._translate(True, True)
|
| 598 |
+
|
| 599 |
+
def test_apply_axis(self):
|
| 600 |
+
df = DataFrame({"A": [0, 0], "B": [1, 1]})
|
| 601 |
+
f = lambda x: [f"val: {x.max()}" for v in x]
|
| 602 |
+
result = df.style.apply(f, axis=1)
|
| 603 |
+
assert len(result._todo) == 1
|
| 604 |
+
assert len(result.ctx) == 0
|
| 605 |
+
result._compute()
|
| 606 |
+
expected = {
|
| 607 |
+
(0, 0): [("val", "1")],
|
| 608 |
+
(0, 1): [("val", "1")],
|
| 609 |
+
(1, 0): [("val", "1")],
|
| 610 |
+
(1, 1): [("val", "1")],
|
| 611 |
+
}
|
| 612 |
+
assert result.ctx == expected
|
| 613 |
+
|
| 614 |
+
result = df.style.apply(f, axis=0)
|
| 615 |
+
expected = {
|
| 616 |
+
(0, 0): [("val", "0")],
|
| 617 |
+
(0, 1): [("val", "1")],
|
| 618 |
+
(1, 0): [("val", "0")],
|
| 619 |
+
(1, 1): [("val", "1")],
|
| 620 |
+
}
|
| 621 |
+
result._compute()
|
| 622 |
+
assert result.ctx == expected
|
| 623 |
+
result = df.style.apply(f) # default
|
| 624 |
+
result._compute()
|
| 625 |
+
assert result.ctx == expected
|
| 626 |
+
|
| 627 |
+
@pytest.mark.parametrize("axis", [0, 1])
|
| 628 |
+
def test_apply_series_return(self, axis):
|
| 629 |
+
# GH 42014
|
| 630 |
+
df = DataFrame([[1, 2], [3, 4]], index=["X", "Y"], columns=["X", "Y"])
|
| 631 |
+
|
| 632 |
+
# test Series return where len(Series) < df.index or df.columns but labels OK
|
| 633 |
+
func = lambda s: Series(["color: red;"], index=["Y"])
|
| 634 |
+
result = df.style.apply(func, axis=axis)._compute().ctx
|
| 635 |
+
assert result[(1, 1)] == [("color", "red")]
|
| 636 |
+
assert result[(1 - axis, axis)] == [("color", "red")]
|
| 637 |
+
|
| 638 |
+
# test Series return where labels align but different order
|
| 639 |
+
func = lambda s: Series(["color: red;", "color: blue;"], index=["Y", "X"])
|
| 640 |
+
result = df.style.apply(func, axis=axis)._compute().ctx
|
| 641 |
+
assert result[(0, 0)] == [("color", "blue")]
|
| 642 |
+
assert result[(1, 1)] == [("color", "red")]
|
| 643 |
+
assert result[(1 - axis, axis)] == [("color", "red")]
|
| 644 |
+
assert result[(axis, 1 - axis)] == [("color", "blue")]
|
| 645 |
+
|
| 646 |
+
@pytest.mark.parametrize("index", [False, True])
|
| 647 |
+
@pytest.mark.parametrize("columns", [False, True])
|
| 648 |
+
def test_apply_dataframe_return(self, index, columns):
|
| 649 |
+
# GH 42014
|
| 650 |
+
df = DataFrame([[1, 2], [3, 4]], index=["X", "Y"], columns=["X", "Y"])
|
| 651 |
+
idxs = ["X", "Y"] if index else ["Y"]
|
| 652 |
+
cols = ["X", "Y"] if columns else ["Y"]
|
| 653 |
+
df_styles = DataFrame("color: red;", index=idxs, columns=cols)
|
| 654 |
+
result = df.style.apply(lambda x: df_styles, axis=None)._compute().ctx
|
| 655 |
+
|
| 656 |
+
assert result[(1, 1)] == [("color", "red")] # (Y,Y) styles always present
|
| 657 |
+
assert (result[(0, 1)] == [("color", "red")]) is index # (X,Y) only if index
|
| 658 |
+
assert (result[(1, 0)] == [("color", "red")]) is columns # (Y,X) only if cols
|
| 659 |
+
assert (result[(0, 0)] == [("color", "red")]) is (index and columns) # (X,X)
|
| 660 |
+
|
| 661 |
+
@pytest.mark.parametrize(
|
| 662 |
+
"slice_",
|
| 663 |
+
[
|
| 664 |
+
IndexSlice[:],
|
| 665 |
+
IndexSlice[:, ["A"]],
|
| 666 |
+
IndexSlice[[1], :],
|
| 667 |
+
IndexSlice[[1], ["A"]],
|
| 668 |
+
IndexSlice[:2, ["A", "B"]],
|
| 669 |
+
],
|
| 670 |
+
)
|
| 671 |
+
@pytest.mark.parametrize("axis", [0, 1])
|
| 672 |
+
def test_apply_subset(self, slice_, axis, df):
|
| 673 |
+
def h(x, color="bar"):
|
| 674 |
+
return Series(f"color: {color}", index=x.index, name=x.name)
|
| 675 |
+
|
| 676 |
+
result = df.style.apply(h, axis=axis, subset=slice_, color="baz")._compute().ctx
|
| 677 |
+
expected = {
|
| 678 |
+
(r, c): [("color", "baz")]
|
| 679 |
+
for r, row in enumerate(df.index)
|
| 680 |
+
for c, col in enumerate(df.columns)
|
| 681 |
+
if row in df.loc[slice_].index and col in df.loc[slice_].columns
|
| 682 |
+
}
|
| 683 |
+
assert result == expected
|
| 684 |
+
|
| 685 |
+
@pytest.mark.parametrize(
|
| 686 |
+
"slice_",
|
| 687 |
+
[
|
| 688 |
+
IndexSlice[:],
|
| 689 |
+
IndexSlice[:, ["A"]],
|
| 690 |
+
IndexSlice[[1], :],
|
| 691 |
+
IndexSlice[[1], ["A"]],
|
| 692 |
+
IndexSlice[:2, ["A", "B"]],
|
| 693 |
+
],
|
| 694 |
+
)
|
| 695 |
+
def test_map_subset(self, slice_, df):
|
| 696 |
+
result = df.style.map(lambda x: "color:baz;", subset=slice_)._compute().ctx
|
| 697 |
+
expected = {
|
| 698 |
+
(r, c): [("color", "baz")]
|
| 699 |
+
for r, row in enumerate(df.index)
|
| 700 |
+
for c, col in enumerate(df.columns)
|
| 701 |
+
if row in df.loc[slice_].index and col in df.loc[slice_].columns
|
| 702 |
+
}
|
| 703 |
+
assert result == expected
|
| 704 |
+
|
| 705 |
+
@pytest.mark.parametrize(
|
| 706 |
+
"slice_",
|
| 707 |
+
[
|
| 708 |
+
IndexSlice[:, IndexSlice["x", "A"]],
|
| 709 |
+
IndexSlice[:, IndexSlice[:, "A"]],
|
| 710 |
+
IndexSlice[:, IndexSlice[:, ["A", "C"]]], # missing col element
|
| 711 |
+
IndexSlice[IndexSlice["a", 1], :],
|
| 712 |
+
IndexSlice[IndexSlice[:, 1], :],
|
| 713 |
+
IndexSlice[IndexSlice[:, [1, 3]], :], # missing row element
|
| 714 |
+
IndexSlice[:, ("x", "A")],
|
| 715 |
+
IndexSlice[("a", 1), :],
|
| 716 |
+
],
|
| 717 |
+
)
|
| 718 |
+
def test_map_subset_multiindex(self, slice_):
|
| 719 |
+
# GH 19861
|
| 720 |
+
# edited for GH 33562
|
| 721 |
+
if (
|
| 722 |
+
isinstance(slice_[-1], tuple)
|
| 723 |
+
and isinstance(slice_[-1][-1], list)
|
| 724 |
+
and "C" in slice_[-1][-1]
|
| 725 |
+
):
|
| 726 |
+
ctx = pytest.raises(KeyError, match="C")
|
| 727 |
+
elif (
|
| 728 |
+
isinstance(slice_[0], tuple)
|
| 729 |
+
and isinstance(slice_[0][1], list)
|
| 730 |
+
and 3 in slice_[0][1]
|
| 731 |
+
):
|
| 732 |
+
ctx = pytest.raises(KeyError, match="3")
|
| 733 |
+
else:
|
| 734 |
+
ctx = contextlib.nullcontext()
|
| 735 |
+
|
| 736 |
+
idx = MultiIndex.from_product([["a", "b"], [1, 2]])
|
| 737 |
+
col = MultiIndex.from_product([["x", "y"], ["A", "B"]])
|
| 738 |
+
df = DataFrame(np.random.default_rng(2).random((4, 4)), columns=col, index=idx)
|
| 739 |
+
|
| 740 |
+
with ctx:
|
| 741 |
+
df.style.map(lambda x: "color: red;", subset=slice_).to_html()
|
| 742 |
+
|
| 743 |
+
def test_map_subset_multiindex_code(self):
|
| 744 |
+
# https://github.com/pandas-dev/pandas/issues/25858
|
| 745 |
+
# Checks styler.map works with multindex when codes are provided
|
| 746 |
+
codes = np.array([[0, 0, 1, 1], [0, 1, 0, 1]])
|
| 747 |
+
columns = MultiIndex(
|
| 748 |
+
levels=[["a", "b"], ["%", "#"]], codes=codes, names=["", ""]
|
| 749 |
+
)
|
| 750 |
+
df = DataFrame(
|
| 751 |
+
[[1, -1, 1, 1], [-1, 1, 1, 1]], index=["hello", "world"], columns=columns
|
| 752 |
+
)
|
| 753 |
+
pct_subset = IndexSlice[:, IndexSlice[:, "%":"%"]]
|
| 754 |
+
|
| 755 |
+
def color_negative_red(val):
|
| 756 |
+
color = "red" if val < 0 else "black"
|
| 757 |
+
return f"color: {color}"
|
| 758 |
+
|
| 759 |
+
df.loc[pct_subset]
|
| 760 |
+
df.style.map(color_negative_red, subset=pct_subset)
|
| 761 |
+
|
| 762 |
+
@pytest.mark.parametrize(
|
| 763 |
+
"stylefunc", ["background_gradient", "bar", "text_gradient"]
|
| 764 |
+
)
|
| 765 |
+
def test_subset_for_boolean_cols(self, stylefunc):
|
| 766 |
+
# GH47838
|
| 767 |
+
df = DataFrame(
|
| 768 |
+
[
|
| 769 |
+
[1, 2],
|
| 770 |
+
[3, 4],
|
| 771 |
+
],
|
| 772 |
+
columns=[False, True],
|
| 773 |
+
)
|
| 774 |
+
styled = getattr(df.style, stylefunc)()
|
| 775 |
+
styled._compute()
|
| 776 |
+
assert set(styled.ctx) == {(0, 0), (0, 1), (1, 0), (1, 1)}
|
| 777 |
+
|
| 778 |
+
def test_empty(self):
|
| 779 |
+
df = DataFrame({"A": [1, 0]})
|
| 780 |
+
s = df.style
|
| 781 |
+
s.ctx = {(0, 0): [("color", "red")], (1, 0): [("", "")]}
|
| 782 |
+
|
| 783 |
+
result = s._translate(True, True)["cellstyle"]
|
| 784 |
+
expected = [
|
| 785 |
+
{"props": [("color", "red")], "selectors": ["row0_col0"]},
|
| 786 |
+
{"props": [("", "")], "selectors": ["row1_col0"]},
|
| 787 |
+
]
|
| 788 |
+
assert result == expected
|
| 789 |
+
|
| 790 |
+
def test_duplicate(self):
|
| 791 |
+
df = DataFrame({"A": [1, 0]})
|
| 792 |
+
s = df.style
|
| 793 |
+
s.ctx = {(0, 0): [("color", "red")], (1, 0): [("color", "red")]}
|
| 794 |
+
|
| 795 |
+
result = s._translate(True, True)["cellstyle"]
|
| 796 |
+
expected = [
|
| 797 |
+
{"props": [("color", "red")], "selectors": ["row0_col0", "row1_col0"]}
|
| 798 |
+
]
|
| 799 |
+
assert result == expected
|
| 800 |
+
|
| 801 |
+
def test_init_with_na_rep(self):
|
| 802 |
+
# GH 21527 28358
|
| 803 |
+
df = DataFrame([[None, None], [1.1, 1.2]], columns=["A", "B"])
|
| 804 |
+
|
| 805 |
+
ctx = Styler(df, na_rep="NA")._translate(True, True)
|
| 806 |
+
assert ctx["body"][0][1]["display_value"] == "NA"
|
| 807 |
+
assert ctx["body"][0][2]["display_value"] == "NA"
|
| 808 |
+
|
| 809 |
+
def test_caption(self, df):
|
| 810 |
+
styler = Styler(df, caption="foo")
|
| 811 |
+
result = styler.to_html()
|
| 812 |
+
assert all(["caption" in result, "foo" in result])
|
| 813 |
+
|
| 814 |
+
styler = df.style
|
| 815 |
+
result = styler.set_caption("baz")
|
| 816 |
+
assert styler is result
|
| 817 |
+
assert styler.caption == "baz"
|
| 818 |
+
|
| 819 |
+
def test_uuid(self, df):
|
| 820 |
+
styler = Styler(df, uuid="abc123")
|
| 821 |
+
result = styler.to_html()
|
| 822 |
+
assert "abc123" in result
|
| 823 |
+
|
| 824 |
+
styler = df.style
|
| 825 |
+
result = styler.set_uuid("aaa")
|
| 826 |
+
assert result is styler
|
| 827 |
+
assert result.uuid == "aaa"
|
| 828 |
+
|
| 829 |
+
def test_unique_id(self):
|
| 830 |
+
# See https://github.com/pandas-dev/pandas/issues/16780
|
| 831 |
+
df = DataFrame({"a": [1, 3, 5, 6], "b": [2, 4, 12, 21]})
|
| 832 |
+
result = df.style.to_html(uuid="test")
|
| 833 |
+
assert "test" in result
|
| 834 |
+
ids = re.findall('id="(.*?)"', result)
|
| 835 |
+
assert np.unique(ids).size == len(ids)
|
| 836 |
+
|
| 837 |
+
def test_table_styles(self, df):
|
| 838 |
+
style = [{"selector": "th", "props": [("foo", "bar")]}] # default format
|
| 839 |
+
styler = Styler(df, table_styles=style)
|
| 840 |
+
result = " ".join(styler.to_html().split())
|
| 841 |
+
assert "th { foo: bar; }" in result
|
| 842 |
+
|
| 843 |
+
styler = df.style
|
| 844 |
+
result = styler.set_table_styles(style)
|
| 845 |
+
assert styler is result
|
| 846 |
+
assert styler.table_styles == style
|
| 847 |
+
|
| 848 |
+
# GH 39563
|
| 849 |
+
style = [{"selector": "th", "props": "foo:bar;"}] # css string format
|
| 850 |
+
styler = df.style.set_table_styles(style)
|
| 851 |
+
result = " ".join(styler.to_html().split())
|
| 852 |
+
assert "th { foo: bar; }" in result
|
| 853 |
+
|
| 854 |
+
def test_table_styles_multiple(self, df):
|
| 855 |
+
ctx = df.style.set_table_styles(
|
| 856 |
+
[
|
| 857 |
+
{"selector": "th,td", "props": "color:red;"},
|
| 858 |
+
{"selector": "tr", "props": "color:green;"},
|
| 859 |
+
]
|
| 860 |
+
)._translate(True, True)["table_styles"]
|
| 861 |
+
assert ctx == [
|
| 862 |
+
{"selector": "th", "props": [("color", "red")]},
|
| 863 |
+
{"selector": "td", "props": [("color", "red")]},
|
| 864 |
+
{"selector": "tr", "props": [("color", "green")]},
|
| 865 |
+
]
|
| 866 |
+
|
| 867 |
+
def test_table_styles_dict_multiple_selectors(self, df):
|
| 868 |
+
# GH 44011
|
| 869 |
+
result = df.style.set_table_styles(
|
| 870 |
+
{
|
| 871 |
+
"B": [
|
| 872 |
+
{"selector": "th,td", "props": [("border-left", "2px solid black")]}
|
| 873 |
+
]
|
| 874 |
+
}
|
| 875 |
+
)._translate(True, True)["table_styles"]
|
| 876 |
+
|
| 877 |
+
expected = [
|
| 878 |
+
{"selector": "th.col1", "props": [("border-left", "2px solid black")]},
|
| 879 |
+
{"selector": "td.col1", "props": [("border-left", "2px solid black")]},
|
| 880 |
+
]
|
| 881 |
+
|
| 882 |
+
assert result == expected
|
| 883 |
+
|
| 884 |
+
def test_maybe_convert_css_to_tuples(self):
|
| 885 |
+
expected = [("a", "b"), ("c", "d e")]
|
| 886 |
+
assert maybe_convert_css_to_tuples("a:b;c:d e;") == expected
|
| 887 |
+
assert maybe_convert_css_to_tuples("a: b ;c: d e ") == expected
|
| 888 |
+
expected = []
|
| 889 |
+
assert maybe_convert_css_to_tuples("") == expected
|
| 890 |
+
|
| 891 |
+
# issue #59623
|
| 892 |
+
expected = [("a", "b"), ("c", "url('data:123')")]
|
| 893 |
+
assert maybe_convert_css_to_tuples("a:b;c: url('data:123');") == expected
|
| 894 |
+
|
| 895 |
+
# if no value, return attr and empty string
|
| 896 |
+
expected = [("a", ""), ("c", "")]
|
| 897 |
+
assert maybe_convert_css_to_tuples("a:;c: ") == expected
|
| 898 |
+
|
| 899 |
+
def test_maybe_convert_css_to_tuples_err(self):
|
| 900 |
+
msg = (
|
| 901 |
+
"Styles supplied as string must follow CSS rule formats, "
|
| 902 |
+
"for example 'attr: val;'. 'err' was given."
|
| 903 |
+
)
|
| 904 |
+
with pytest.raises(ValueError, match=msg):
|
| 905 |
+
maybe_convert_css_to_tuples("err")
|
| 906 |
+
|
| 907 |
+
def test_table_attributes(self, df):
|
| 908 |
+
attributes = 'class="foo" data-bar'
|
| 909 |
+
styler = Styler(df, table_attributes=attributes)
|
| 910 |
+
result = styler.to_html()
|
| 911 |
+
assert 'class="foo" data-bar' in result
|
| 912 |
+
|
| 913 |
+
result = df.style.set_table_attributes(attributes).to_html()
|
| 914 |
+
assert 'class="foo" data-bar' in result
|
| 915 |
+
|
| 916 |
+
def test_apply_none(self):
|
| 917 |
+
def f(x):
|
| 918 |
+
return DataFrame(
|
| 919 |
+
np.where(x == x.max(), "color: red", ""),
|
| 920 |
+
index=x.index,
|
| 921 |
+
columns=x.columns,
|
| 922 |
+
)
|
| 923 |
+
|
| 924 |
+
result = DataFrame([[1, 2], [3, 4]]).style.apply(f, axis=None)._compute().ctx
|
| 925 |
+
assert result[(1, 1)] == [("color", "red")]
|
| 926 |
+
|
| 927 |
+
def test_trim(self, df):
|
| 928 |
+
result = df.style.to_html() # trim=True
|
| 929 |
+
assert result.count("#") == 0
|
| 930 |
+
|
| 931 |
+
result = df.style.highlight_max().to_html()
|
| 932 |
+
assert result.count("#") == len(df.columns)
|
| 933 |
+
|
| 934 |
+
def test_export(self, df, styler):
|
| 935 |
+
f = lambda x: "color: red" if x > 0 else "color: blue"
|
| 936 |
+
g = lambda x, z: f"color: {z}"
|
| 937 |
+
style1 = styler
|
| 938 |
+
style1.map(f).map(g, z="b").highlight_max()._compute() # = render
|
| 939 |
+
result = style1.export()
|
| 940 |
+
style2 = df.style
|
| 941 |
+
style2.use(result)
|
| 942 |
+
assert style1._todo == style2._todo
|
| 943 |
+
style2.to_html()
|
| 944 |
+
|
| 945 |
+
def test_bad_apply_shape(self):
|
| 946 |
+
df = DataFrame([[1, 2], [3, 4]], index=["A", "B"], columns=["X", "Y"])
|
| 947 |
+
|
| 948 |
+
msg = "resulted in the apply method collapsing to a Series."
|
| 949 |
+
with pytest.raises(ValueError, match=msg):
|
| 950 |
+
df.style._apply(lambda x: "x")
|
| 951 |
+
|
| 952 |
+
msg = "created invalid {} labels"
|
| 953 |
+
with pytest.raises(ValueError, match=msg.format("index")):
|
| 954 |
+
df.style._apply(lambda x: [""])
|
| 955 |
+
|
| 956 |
+
with pytest.raises(ValueError, match=msg.format("index")):
|
| 957 |
+
df.style._apply(lambda x: ["", "", "", ""])
|
| 958 |
+
|
| 959 |
+
with pytest.raises(ValueError, match=msg.format("index")):
|
| 960 |
+
df.style._apply(lambda x: Series(["a:v;", ""], index=["A", "C"]), axis=0)
|
| 961 |
+
|
| 962 |
+
with pytest.raises(ValueError, match=msg.format("columns")):
|
| 963 |
+
df.style._apply(lambda x: ["", "", ""], axis=1)
|
| 964 |
+
|
| 965 |
+
with pytest.raises(ValueError, match=msg.format("columns")):
|
| 966 |
+
df.style._apply(lambda x: Series(["a:v;", ""], index=["X", "Z"]), axis=1)
|
| 967 |
+
|
| 968 |
+
msg = "returned ndarray with wrong shape"
|
| 969 |
+
with pytest.raises(ValueError, match=msg):
|
| 970 |
+
df.style._apply(lambda x: np.array([[""], [""]]), axis=None)
|
| 971 |
+
|
| 972 |
+
def test_apply_bad_return(self):
|
| 973 |
+
def f(x):
|
| 974 |
+
return ""
|
| 975 |
+
|
| 976 |
+
df = DataFrame([[1, 2], [3, 4]])
|
| 977 |
+
msg = (
|
| 978 |
+
"must return a DataFrame or ndarray when passed to `Styler.apply` "
|
| 979 |
+
"with axis=None"
|
| 980 |
+
)
|
| 981 |
+
with pytest.raises(TypeError, match=msg):
|
| 982 |
+
df.style._apply(f, axis=None)
|
| 983 |
+
|
| 984 |
+
@pytest.mark.parametrize("axis", ["index", "columns"])
|
| 985 |
+
def test_apply_bad_labels(self, axis):
|
| 986 |
+
def f(x):
|
| 987 |
+
return DataFrame(**{axis: ["bad", "labels"]})
|
| 988 |
+
|
| 989 |
+
df = DataFrame([[1, 2], [3, 4]])
|
| 990 |
+
msg = f"created invalid {axis} labels."
|
| 991 |
+
with pytest.raises(ValueError, match=msg):
|
| 992 |
+
df.style._apply(f, axis=None)
|
| 993 |
+
|
| 994 |
+
def test_get_level_lengths(self):
|
| 995 |
+
index = MultiIndex.from_product([["a", "b"], [0, 1, 2]])
|
| 996 |
+
expected = {
|
| 997 |
+
(0, 0): 3,
|
| 998 |
+
(0, 3): 3,
|
| 999 |
+
(1, 0): 1,
|
| 1000 |
+
(1, 1): 1,
|
| 1001 |
+
(1, 2): 1,
|
| 1002 |
+
(1, 3): 1,
|
| 1003 |
+
(1, 4): 1,
|
| 1004 |
+
(1, 5): 1,
|
| 1005 |
+
}
|
| 1006 |
+
result = _get_level_lengths(index, sparsify=True, max_index=100)
|
| 1007 |
+
tm.assert_dict_equal(result, expected)
|
| 1008 |
+
|
| 1009 |
+
expected = {
|
| 1010 |
+
(0, 0): 1,
|
| 1011 |
+
(0, 1): 1,
|
| 1012 |
+
(0, 2): 1,
|
| 1013 |
+
(0, 3): 1,
|
| 1014 |
+
(0, 4): 1,
|
| 1015 |
+
(0, 5): 1,
|
| 1016 |
+
(1, 0): 1,
|
| 1017 |
+
(1, 1): 1,
|
| 1018 |
+
(1, 2): 1,
|
| 1019 |
+
(1, 3): 1,
|
| 1020 |
+
(1, 4): 1,
|
| 1021 |
+
(1, 5): 1,
|
| 1022 |
+
}
|
| 1023 |
+
result = _get_level_lengths(index, sparsify=False, max_index=100)
|
| 1024 |
+
tm.assert_dict_equal(result, expected)
|
| 1025 |
+
|
| 1026 |
+
def test_get_level_lengths_un_sorted(self):
|
| 1027 |
+
index = MultiIndex.from_arrays([[1, 1, 2, 1], ["a", "b", "b", "d"]])
|
| 1028 |
+
expected = {
|
| 1029 |
+
(0, 0): 2,
|
| 1030 |
+
(0, 2): 1,
|
| 1031 |
+
(0, 3): 1,
|
| 1032 |
+
(1, 0): 1,
|
| 1033 |
+
(1, 1): 1,
|
| 1034 |
+
(1, 2): 1,
|
| 1035 |
+
(1, 3): 1,
|
| 1036 |
+
}
|
| 1037 |
+
result = _get_level_lengths(index, sparsify=True, max_index=100)
|
| 1038 |
+
tm.assert_dict_equal(result, expected)
|
| 1039 |
+
|
| 1040 |
+
expected = {
|
| 1041 |
+
(0, 0): 1,
|
| 1042 |
+
(0, 1): 1,
|
| 1043 |
+
(0, 2): 1,
|
| 1044 |
+
(0, 3): 1,
|
| 1045 |
+
(1, 0): 1,
|
| 1046 |
+
(1, 1): 1,
|
| 1047 |
+
(1, 2): 1,
|
| 1048 |
+
(1, 3): 1,
|
| 1049 |
+
}
|
| 1050 |
+
result = _get_level_lengths(index, sparsify=False, max_index=100)
|
| 1051 |
+
tm.assert_dict_equal(result, expected)
|
| 1052 |
+
|
| 1053 |
+
def test_mi_sparse_index_names(self, blank_value):
|
| 1054 |
+
# Test the class names and displayed value are correct on rendering MI names
|
| 1055 |
+
df = DataFrame(
|
| 1056 |
+
{"A": [1, 2]},
|
| 1057 |
+
index=MultiIndex.from_arrays(
|
| 1058 |
+
[["a", "a"], [0, 1]], names=["idx_level_0", "idx_level_1"]
|
| 1059 |
+
),
|
| 1060 |
+
)
|
| 1061 |
+
result = df.style._translate(True, True)
|
| 1062 |
+
head = result["head"][1]
|
| 1063 |
+
expected = [
|
| 1064 |
+
{
|
| 1065 |
+
"class": "index_name level0",
|
| 1066 |
+
"display_value": "idx_level_0",
|
| 1067 |
+
"is_visible": True,
|
| 1068 |
+
},
|
| 1069 |
+
{
|
| 1070 |
+
"class": "index_name level1",
|
| 1071 |
+
"display_value": "idx_level_1",
|
| 1072 |
+
"is_visible": True,
|
| 1073 |
+
},
|
| 1074 |
+
{
|
| 1075 |
+
"class": "blank col0",
|
| 1076 |
+
"display_value": blank_value,
|
| 1077 |
+
"is_visible": True,
|
| 1078 |
+
},
|
| 1079 |
+
]
|
| 1080 |
+
for i, expected_dict in enumerate(expected):
|
| 1081 |
+
assert expected_dict.items() <= head[i].items()
|
| 1082 |
+
|
| 1083 |
+
def test_mi_sparse_column_names(self, blank_value):
|
| 1084 |
+
df = DataFrame(
|
| 1085 |
+
np.arange(16).reshape(4, 4),
|
| 1086 |
+
index=MultiIndex.from_arrays(
|
| 1087 |
+
[["a", "a", "b", "a"], [0, 1, 1, 2]],
|
| 1088 |
+
names=["idx_level_0", "idx_level_1"],
|
| 1089 |
+
),
|
| 1090 |
+
columns=MultiIndex.from_arrays(
|
| 1091 |
+
[["C1", "C1", "C2", "C2"], [1, 0, 1, 0]], names=["colnam_0", "colnam_1"]
|
| 1092 |
+
),
|
| 1093 |
+
)
|
| 1094 |
+
result = Styler(df, cell_ids=False)._translate(True, True)
|
| 1095 |
+
|
| 1096 |
+
for level in [0, 1]:
|
| 1097 |
+
head = result["head"][level]
|
| 1098 |
+
expected = [
|
| 1099 |
+
{
|
| 1100 |
+
"class": "blank",
|
| 1101 |
+
"display_value": blank_value,
|
| 1102 |
+
"is_visible": True,
|
| 1103 |
+
},
|
| 1104 |
+
{
|
| 1105 |
+
"class": f"index_name level{level}",
|
| 1106 |
+
"display_value": f"colnam_{level}",
|
| 1107 |
+
"is_visible": True,
|
| 1108 |
+
},
|
| 1109 |
+
]
|
| 1110 |
+
for i, expected_dict in enumerate(expected):
|
| 1111 |
+
assert expected_dict.items() <= head[i].items()
|
| 1112 |
+
|
| 1113 |
+
def test_hide_column_headers(self, df, styler):
|
| 1114 |
+
ctx = styler.hide(axis="columns")._translate(True, True)
|
| 1115 |
+
assert len(ctx["head"]) == 0 # no header entries with an unnamed index
|
| 1116 |
+
|
| 1117 |
+
df.index.name = "some_name"
|
| 1118 |
+
ctx = df.style.hide(axis="columns")._translate(True, True)
|
| 1119 |
+
assert len(ctx["head"]) == 1
|
| 1120 |
+
# index names still visible, changed in #42101, reverted in 43404
|
| 1121 |
+
|
| 1122 |
+
def test_hide_single_index(self, df):
|
| 1123 |
+
# GH 14194
|
| 1124 |
+
# single unnamed index
|
| 1125 |
+
ctx = df.style._translate(True, True)
|
| 1126 |
+
assert ctx["body"][0][0]["is_visible"]
|
| 1127 |
+
assert ctx["head"][0][0]["is_visible"]
|
| 1128 |
+
ctx2 = df.style.hide(axis="index")._translate(True, True)
|
| 1129 |
+
assert not ctx2["body"][0][0]["is_visible"]
|
| 1130 |
+
assert not ctx2["head"][0][0]["is_visible"]
|
| 1131 |
+
|
| 1132 |
+
# single named index
|
| 1133 |
+
ctx3 = df.set_index("A").style._translate(True, True)
|
| 1134 |
+
assert ctx3["body"][0][0]["is_visible"]
|
| 1135 |
+
assert len(ctx3["head"]) == 2 # 2 header levels
|
| 1136 |
+
assert ctx3["head"][0][0]["is_visible"]
|
| 1137 |
+
|
| 1138 |
+
ctx4 = df.set_index("A").style.hide(axis="index")._translate(True, True)
|
| 1139 |
+
assert not ctx4["body"][0][0]["is_visible"]
|
| 1140 |
+
assert len(ctx4["head"]) == 1 # only 1 header levels
|
| 1141 |
+
assert not ctx4["head"][0][0]["is_visible"]
|
| 1142 |
+
|
| 1143 |
+
def test_hide_multiindex(self):
|
| 1144 |
+
# GH 14194
|
| 1145 |
+
df = DataFrame(
|
| 1146 |
+
{"A": [1, 2], "B": [1, 2]},
|
| 1147 |
+
index=MultiIndex.from_arrays(
|
| 1148 |
+
[["a", "a"], [0, 1]], names=["idx_level_0", "idx_level_1"]
|
| 1149 |
+
),
|
| 1150 |
+
)
|
| 1151 |
+
ctx1 = df.style._translate(True, True)
|
| 1152 |
+
# tests for 'a' and '0'
|
| 1153 |
+
assert ctx1["body"][0][0]["is_visible"]
|
| 1154 |
+
assert ctx1["body"][0][1]["is_visible"]
|
| 1155 |
+
# check for blank header rows
|
| 1156 |
+
assert len(ctx1["head"][0]) == 4 # two visible indexes and two data columns
|
| 1157 |
+
|
| 1158 |
+
ctx2 = df.style.hide(axis="index")._translate(True, True)
|
| 1159 |
+
# tests for 'a' and '0'
|
| 1160 |
+
assert not ctx2["body"][0][0]["is_visible"]
|
| 1161 |
+
assert not ctx2["body"][0][1]["is_visible"]
|
| 1162 |
+
# check for blank header rows
|
| 1163 |
+
assert len(ctx2["head"][0]) == 3 # one hidden (col name) and two data columns
|
| 1164 |
+
assert not ctx2["head"][0][0]["is_visible"]
|
| 1165 |
+
|
| 1166 |
+
def test_hide_columns_single_level(self, df):
|
| 1167 |
+
# GH 14194
|
| 1168 |
+
# test hiding single column
|
| 1169 |
+
ctx = df.style._translate(True, True)
|
| 1170 |
+
assert ctx["head"][0][1]["is_visible"]
|
| 1171 |
+
assert ctx["head"][0][1]["display_value"] == "A"
|
| 1172 |
+
assert ctx["head"][0][2]["is_visible"]
|
| 1173 |
+
assert ctx["head"][0][2]["display_value"] == "B"
|
| 1174 |
+
assert ctx["body"][0][1]["is_visible"] # col A, row 1
|
| 1175 |
+
assert ctx["body"][1][2]["is_visible"] # col B, row 1
|
| 1176 |
+
|
| 1177 |
+
ctx = df.style.hide("A", axis="columns")._translate(True, True)
|
| 1178 |
+
assert not ctx["head"][0][1]["is_visible"]
|
| 1179 |
+
assert not ctx["body"][0][1]["is_visible"] # col A, row 1
|
| 1180 |
+
assert ctx["body"][1][2]["is_visible"] # col B, row 1
|
| 1181 |
+
|
| 1182 |
+
# test hiding multiple columns
|
| 1183 |
+
ctx = df.style.hide(["A", "B"], axis="columns")._translate(True, True)
|
| 1184 |
+
assert not ctx["head"][0][1]["is_visible"]
|
| 1185 |
+
assert not ctx["head"][0][2]["is_visible"]
|
| 1186 |
+
assert not ctx["body"][0][1]["is_visible"] # col A, row 1
|
| 1187 |
+
assert not ctx["body"][1][2]["is_visible"] # col B, row 1
|
| 1188 |
+
|
| 1189 |
+
def test_hide_columns_index_mult_levels(self):
|
| 1190 |
+
# GH 14194
|
| 1191 |
+
# setup dataframe with multiple column levels and indices
|
| 1192 |
+
i1 = MultiIndex.from_arrays(
|
| 1193 |
+
[["a", "a"], [0, 1]], names=["idx_level_0", "idx_level_1"]
|
| 1194 |
+
)
|
| 1195 |
+
i2 = MultiIndex.from_arrays(
|
| 1196 |
+
[["b", "b"], [0, 1]], names=["col_level_0", "col_level_1"]
|
| 1197 |
+
)
|
| 1198 |
+
df = DataFrame([[1, 2], [3, 4]], index=i1, columns=i2)
|
| 1199 |
+
ctx = df.style._translate(True, True)
|
| 1200 |
+
# column headers
|
| 1201 |
+
assert ctx["head"][0][2]["is_visible"]
|
| 1202 |
+
assert ctx["head"][1][2]["is_visible"]
|
| 1203 |
+
assert ctx["head"][1][3]["display_value"] == "1"
|
| 1204 |
+
# indices
|
| 1205 |
+
assert ctx["body"][0][0]["is_visible"]
|
| 1206 |
+
# data
|
| 1207 |
+
assert ctx["body"][1][2]["is_visible"]
|
| 1208 |
+
assert ctx["body"][1][2]["display_value"] == "3"
|
| 1209 |
+
assert ctx["body"][1][3]["is_visible"]
|
| 1210 |
+
assert ctx["body"][1][3]["display_value"] == "4"
|
| 1211 |
+
|
| 1212 |
+
# hide top column level, which hides both columns
|
| 1213 |
+
ctx = df.style.hide("b", axis="columns")._translate(True, True)
|
| 1214 |
+
assert not ctx["head"][0][2]["is_visible"] # b
|
| 1215 |
+
assert not ctx["head"][1][2]["is_visible"] # 0
|
| 1216 |
+
assert not ctx["body"][1][2]["is_visible"] # 3
|
| 1217 |
+
assert ctx["body"][0][0]["is_visible"] # index
|
| 1218 |
+
|
| 1219 |
+
# hide first column only
|
| 1220 |
+
ctx = df.style.hide([("b", 0)], axis="columns")._translate(True, True)
|
| 1221 |
+
assert not ctx["head"][0][2]["is_visible"] # b
|
| 1222 |
+
assert ctx["head"][0][3]["is_visible"] # b
|
| 1223 |
+
assert not ctx["head"][1][2]["is_visible"] # 0
|
| 1224 |
+
assert not ctx["body"][1][2]["is_visible"] # 3
|
| 1225 |
+
assert ctx["body"][1][3]["is_visible"]
|
| 1226 |
+
assert ctx["body"][1][3]["display_value"] == "4"
|
| 1227 |
+
|
| 1228 |
+
# hide second column and index
|
| 1229 |
+
ctx = df.style.hide([("b", 1)], axis=1).hide(axis=0)._translate(True, True)
|
| 1230 |
+
assert not ctx["body"][0][0]["is_visible"] # index
|
| 1231 |
+
assert len(ctx["head"][0]) == 3
|
| 1232 |
+
assert ctx["head"][0][1]["is_visible"] # b
|
| 1233 |
+
assert ctx["head"][1][1]["is_visible"] # 0
|
| 1234 |
+
assert not ctx["head"][1][2]["is_visible"] # 1
|
| 1235 |
+
assert not ctx["body"][1][3]["is_visible"] # 4
|
| 1236 |
+
assert ctx["body"][1][2]["is_visible"]
|
| 1237 |
+
assert ctx["body"][1][2]["display_value"] == "3"
|
| 1238 |
+
|
| 1239 |
+
# hide top row level, which hides both rows so body empty
|
| 1240 |
+
ctx = df.style.hide("a", axis="index")._translate(True, True)
|
| 1241 |
+
assert ctx["body"] == []
|
| 1242 |
+
|
| 1243 |
+
# hide first row only
|
| 1244 |
+
ctx = df.style.hide(("a", 0), axis="index")._translate(True, True)
|
| 1245 |
+
for i in [0, 1, 2, 3]:
|
| 1246 |
+
assert "row1" in ctx["body"][0][i]["class"] # row0 not included in body
|
| 1247 |
+
assert ctx["body"][0][i]["is_visible"]
|
| 1248 |
+
|
| 1249 |
+
def test_pipe(self, df):
|
| 1250 |
+
def set_caption_from_template(styler, a, b):
|
| 1251 |
+
return styler.set_caption(f"Dataframe with a = {a} and b = {b}")
|
| 1252 |
+
|
| 1253 |
+
styler = df.style.pipe(set_caption_from_template, "A", b="B")
|
| 1254 |
+
assert "Dataframe with a = A and b = B" in styler.to_html()
|
| 1255 |
+
|
| 1256 |
+
# Test with an argument that is a (callable, keyword_name) pair.
|
| 1257 |
+
def f(a, b, styler):
|
| 1258 |
+
return (a, b, styler)
|
| 1259 |
+
|
| 1260 |
+
styler = df.style
|
| 1261 |
+
result = styler.pipe((f, "styler"), a=1, b=2)
|
| 1262 |
+
assert result == (1, 2, styler)
|
| 1263 |
+
|
| 1264 |
+
def test_no_cell_ids(self):
|
| 1265 |
+
# GH 35588
|
| 1266 |
+
# GH 35663
|
| 1267 |
+
df = DataFrame(data=[[0]])
|
| 1268 |
+
styler = Styler(df, uuid="_", cell_ids=False)
|
| 1269 |
+
styler.to_html()
|
| 1270 |
+
s = styler.to_html() # render twice to ensure ctx is not updated
|
| 1271 |
+
assert s.find('<td class="data row0 col0" >') != -1
|
| 1272 |
+
|
| 1273 |
+
@pytest.mark.parametrize(
|
| 1274 |
+
"classes",
|
| 1275 |
+
[
|
| 1276 |
+
DataFrame(
|
| 1277 |
+
data=[["", "test-class"], [np.nan, None]],
|
| 1278 |
+
columns=["A", "B"],
|
| 1279 |
+
index=["a", "b"],
|
| 1280 |
+
),
|
| 1281 |
+
DataFrame(data=[["test-class"]], columns=["B"], index=["a"]),
|
| 1282 |
+
DataFrame(data=[["test-class", "unused"]], columns=["B", "C"], index=["a"]),
|
| 1283 |
+
],
|
| 1284 |
+
)
|
| 1285 |
+
def test_set_data_classes(self, classes):
|
| 1286 |
+
# GH 36159
|
| 1287 |
+
df = DataFrame(data=[[0, 1], [2, 3]], columns=["A", "B"], index=["a", "b"])
|
| 1288 |
+
s = Styler(df, uuid_len=0, cell_ids=False).set_td_classes(classes).to_html()
|
| 1289 |
+
assert '<td class="data row0 col0" >0</td>' in s
|
| 1290 |
+
assert '<td class="data row0 col1 test-class" >1</td>' in s
|
| 1291 |
+
assert '<td class="data row1 col0" >2</td>' in s
|
| 1292 |
+
assert '<td class="data row1 col1" >3</td>' in s
|
| 1293 |
+
# GH 39317
|
| 1294 |
+
s = Styler(df, uuid_len=0, cell_ids=True).set_td_classes(classes).to_html()
|
| 1295 |
+
assert '<td id="T__row0_col0" class="data row0 col0" >0</td>' in s
|
| 1296 |
+
assert '<td id="T__row0_col1" class="data row0 col1 test-class" >1</td>' in s
|
| 1297 |
+
assert '<td id="T__row1_col0" class="data row1 col0" >2</td>' in s
|
| 1298 |
+
assert '<td id="T__row1_col1" class="data row1 col1" >3</td>' in s
|
| 1299 |
+
|
| 1300 |
+
def test_set_data_classes_reindex(self):
|
| 1301 |
+
# GH 39317
|
| 1302 |
+
df = DataFrame(
|
| 1303 |
+
data=[[0, 1, 2], [3, 4, 5], [6, 7, 8]], columns=[0, 1, 2], index=[0, 1, 2]
|
| 1304 |
+
)
|
| 1305 |
+
classes = DataFrame(
|
| 1306 |
+
data=[["mi", "ma"], ["mu", "mo"]],
|
| 1307 |
+
columns=[0, 2],
|
| 1308 |
+
index=[0, 2],
|
| 1309 |
+
)
|
| 1310 |
+
s = Styler(df, uuid_len=0).set_td_classes(classes).to_html()
|
| 1311 |
+
assert '<td id="T__row0_col0" class="data row0 col0 mi" >0</td>' in s
|
| 1312 |
+
assert '<td id="T__row0_col2" class="data row0 col2 ma" >2</td>' in s
|
| 1313 |
+
assert '<td id="T__row1_col1" class="data row1 col1" >4</td>' in s
|
| 1314 |
+
assert '<td id="T__row2_col0" class="data row2 col0 mu" >6</td>' in s
|
| 1315 |
+
assert '<td id="T__row2_col2" class="data row2 col2 mo" >8</td>' in s
|
| 1316 |
+
|
| 1317 |
+
def test_chaining_table_styles(self):
|
| 1318 |
+
# GH 35607
|
| 1319 |
+
df = DataFrame(data=[[0, 1], [1, 2]], columns=["A", "B"])
|
| 1320 |
+
styler = df.style.set_table_styles(
|
| 1321 |
+
[{"selector": "", "props": [("background-color", "yellow")]}]
|
| 1322 |
+
).set_table_styles(
|
| 1323 |
+
[{"selector": ".col0", "props": [("background-color", "blue")]}],
|
| 1324 |
+
overwrite=False,
|
| 1325 |
+
)
|
| 1326 |
+
assert len(styler.table_styles) == 2
|
| 1327 |
+
|
| 1328 |
+
def test_column_and_row_styling(self):
|
| 1329 |
+
# GH 35607
|
| 1330 |
+
df = DataFrame(data=[[0, 1], [1, 2]], columns=["A", "B"])
|
| 1331 |
+
s = Styler(df, uuid_len=0)
|
| 1332 |
+
s = s.set_table_styles({"A": [{"selector": "", "props": [("color", "blue")]}]})
|
| 1333 |
+
assert "#T_ .col0 {\n color: blue;\n}" in s.to_html()
|
| 1334 |
+
s = s.set_table_styles(
|
| 1335 |
+
{0: [{"selector": "", "props": [("color", "blue")]}]}, axis=1
|
| 1336 |
+
)
|
| 1337 |
+
assert "#T_ .row0 {\n color: blue;\n}" in s.to_html()
|
| 1338 |
+
|
| 1339 |
+
@pytest.mark.parametrize("len_", [1, 5, 32, 33, 100])
|
| 1340 |
+
def test_uuid_len(self, len_):
|
| 1341 |
+
# GH 36345
|
| 1342 |
+
df = DataFrame(data=[["A"]])
|
| 1343 |
+
s = Styler(df, uuid_len=len_, cell_ids=False).to_html()
|
| 1344 |
+
strt = s.find('id="T_')
|
| 1345 |
+
end = s[strt + 6 :].find('"')
|
| 1346 |
+
if len_ > 32:
|
| 1347 |
+
assert end == 32
|
| 1348 |
+
else:
|
| 1349 |
+
assert end == len_
|
| 1350 |
+
|
| 1351 |
+
@pytest.mark.parametrize("len_", [-2, "bad", None])
|
| 1352 |
+
def test_uuid_len_raises(self, len_):
|
| 1353 |
+
# GH 36345
|
| 1354 |
+
df = DataFrame(data=[["A"]])
|
| 1355 |
+
msg = "``uuid_len`` must be an integer in range \\[0, 32\\]."
|
| 1356 |
+
with pytest.raises(TypeError, match=msg):
|
| 1357 |
+
Styler(df, uuid_len=len_, cell_ids=False).to_html()
|
| 1358 |
+
|
| 1359 |
+
@pytest.mark.parametrize(
|
| 1360 |
+
"slc",
|
| 1361 |
+
[
|
| 1362 |
+
IndexSlice[:, :],
|
| 1363 |
+
IndexSlice[:, 1],
|
| 1364 |
+
IndexSlice[1, :],
|
| 1365 |
+
IndexSlice[[1], [1]],
|
| 1366 |
+
IndexSlice[1, [1]],
|
| 1367 |
+
IndexSlice[[1], 1],
|
| 1368 |
+
IndexSlice[1],
|
| 1369 |
+
IndexSlice[1, 1],
|
| 1370 |
+
slice(None, None, None),
|
| 1371 |
+
[0, 1],
|
| 1372 |
+
np.array([0, 1]),
|
| 1373 |
+
Series([0, 1]),
|
| 1374 |
+
],
|
| 1375 |
+
)
|
| 1376 |
+
def test_non_reducing_slice(self, slc):
|
| 1377 |
+
df = DataFrame([[0, 1], [2, 3]])
|
| 1378 |
+
|
| 1379 |
+
tslice_ = non_reducing_slice(slc)
|
| 1380 |
+
assert isinstance(df.loc[tslice_], DataFrame)
|
| 1381 |
+
|
| 1382 |
+
@pytest.mark.parametrize("box", [list, Series, np.array])
|
| 1383 |
+
def test_list_slice(self, box):
|
| 1384 |
+
# like dataframe getitem
|
| 1385 |
+
subset = box(["A"])
|
| 1386 |
+
|
| 1387 |
+
df = DataFrame({"A": [1, 2], "B": [3, 4]}, index=["A", "B"])
|
| 1388 |
+
expected = IndexSlice[:, ["A"]]
|
| 1389 |
+
|
| 1390 |
+
result = non_reducing_slice(subset)
|
| 1391 |
+
tm.assert_frame_equal(df.loc[result], df.loc[expected])
|
| 1392 |
+
|
| 1393 |
+
def test_non_reducing_slice_on_multiindex(self):
|
| 1394 |
+
# GH 19861
|
| 1395 |
+
dic = {
|
| 1396 |
+
("a", "d"): [1, 4],
|
| 1397 |
+
("a", "c"): [2, 3],
|
| 1398 |
+
("b", "c"): [3, 2],
|
| 1399 |
+
("b", "d"): [4, 1],
|
| 1400 |
+
}
|
| 1401 |
+
df = DataFrame(dic, index=[0, 1])
|
| 1402 |
+
idx = IndexSlice
|
| 1403 |
+
slice_ = idx[:, idx["b", "d"]]
|
| 1404 |
+
tslice_ = non_reducing_slice(slice_)
|
| 1405 |
+
|
| 1406 |
+
result = df.loc[tslice_]
|
| 1407 |
+
expected = DataFrame({("b", "d"): [4, 1]})
|
| 1408 |
+
tm.assert_frame_equal(result, expected)
|
| 1409 |
+
|
| 1410 |
+
@pytest.mark.parametrize(
|
| 1411 |
+
"slice_",
|
| 1412 |
+
[
|
| 1413 |
+
IndexSlice[:, :],
|
| 1414 |
+
# check cols
|
| 1415 |
+
IndexSlice[:, IndexSlice[["a"]]], # inferred deeper need list
|
| 1416 |
+
IndexSlice[:, IndexSlice[["a"], ["c"]]], # inferred deeper need list
|
| 1417 |
+
IndexSlice[:, IndexSlice["a", "c", :]],
|
| 1418 |
+
IndexSlice[:, IndexSlice["a", :, "e"]],
|
| 1419 |
+
IndexSlice[:, IndexSlice[:, "c", "e"]],
|
| 1420 |
+
IndexSlice[:, IndexSlice["a", ["c", "d"], :]], # check list
|
| 1421 |
+
IndexSlice[:, IndexSlice["a", ["c", "d", "-"], :]], # don't allow missing
|
| 1422 |
+
IndexSlice[:, IndexSlice["a", ["c", "d", "-"], "e"]], # no slice
|
| 1423 |
+
# check rows
|
| 1424 |
+
IndexSlice[IndexSlice[["U"]], :], # inferred deeper need list
|
| 1425 |
+
IndexSlice[IndexSlice[["U"], ["W"]], :], # inferred deeper need list
|
| 1426 |
+
IndexSlice[IndexSlice["U", "W", :], :],
|
| 1427 |
+
IndexSlice[IndexSlice["U", :, "Y"], :],
|
| 1428 |
+
IndexSlice[IndexSlice[:, "W", "Y"], :],
|
| 1429 |
+
IndexSlice[IndexSlice[:, "W", ["Y", "Z"]], :], # check list
|
| 1430 |
+
IndexSlice[IndexSlice[:, "W", ["Y", "Z", "-"]], :], # don't allow missing
|
| 1431 |
+
IndexSlice[IndexSlice["U", "W", ["Y", "Z", "-"]], :], # no slice
|
| 1432 |
+
# check simultaneous
|
| 1433 |
+
IndexSlice[IndexSlice[:, "W", "Y"], IndexSlice["a", "c", :]],
|
| 1434 |
+
],
|
| 1435 |
+
)
|
| 1436 |
+
def test_non_reducing_multi_slice_on_multiindex(self, slice_):
|
| 1437 |
+
# GH 33562
|
| 1438 |
+
cols = MultiIndex.from_product([["a", "b"], ["c", "d"], ["e", "f"]])
|
| 1439 |
+
idxs = MultiIndex.from_product([["U", "V"], ["W", "X"], ["Y", "Z"]])
|
| 1440 |
+
df = DataFrame(np.arange(64).reshape(8, 8), columns=cols, index=idxs)
|
| 1441 |
+
|
| 1442 |
+
for lvl in [0, 1]:
|
| 1443 |
+
key = slice_[lvl]
|
| 1444 |
+
if isinstance(key, tuple):
|
| 1445 |
+
for subkey in key:
|
| 1446 |
+
if isinstance(subkey, list) and "-" in subkey:
|
| 1447 |
+
# not present in the index level, raises KeyError since 2.0
|
| 1448 |
+
with pytest.raises(KeyError, match="-"):
|
| 1449 |
+
df.loc[slice_]
|
| 1450 |
+
return
|
| 1451 |
+
|
| 1452 |
+
expected = df.loc[slice_]
|
| 1453 |
+
result = df.loc[non_reducing_slice(slice_)]
|
| 1454 |
+
tm.assert_frame_equal(result, expected)
|
| 1455 |
+
|
| 1456 |
+
|
| 1457 |
+
def test_hidden_index_names(mi_df):
|
| 1458 |
+
mi_df.index.names = ["Lev0", "Lev1"]
|
| 1459 |
+
mi_styler = mi_df.style
|
| 1460 |
+
ctx = mi_styler._translate(True, True)
|
| 1461 |
+
assert len(ctx["head"]) == 3 # 2 column index levels + 1 index names row
|
| 1462 |
+
|
| 1463 |
+
mi_styler.hide(axis="index", names=True)
|
| 1464 |
+
ctx = mi_styler._translate(True, True)
|
| 1465 |
+
assert len(ctx["head"]) == 2 # index names row is unparsed
|
| 1466 |
+
for i in range(4):
|
| 1467 |
+
assert ctx["body"][0][i]["is_visible"] # 2 index levels + 2 data values visible
|
| 1468 |
+
|
| 1469 |
+
mi_styler.hide(axis="index", level=1)
|
| 1470 |
+
ctx = mi_styler._translate(True, True)
|
| 1471 |
+
assert len(ctx["head"]) == 2 # index names row is still hidden
|
| 1472 |
+
assert ctx["body"][0][0]["is_visible"] is True
|
| 1473 |
+
assert ctx["body"][0][1]["is_visible"] is False
|
| 1474 |
+
|
| 1475 |
+
|
| 1476 |
+
def test_hidden_column_names(mi_df):
|
| 1477 |
+
mi_df.columns.names = ["Lev0", "Lev1"]
|
| 1478 |
+
mi_styler = mi_df.style
|
| 1479 |
+
ctx = mi_styler._translate(True, True)
|
| 1480 |
+
assert ctx["head"][0][1]["display_value"] == "Lev0"
|
| 1481 |
+
assert ctx["head"][1][1]["display_value"] == "Lev1"
|
| 1482 |
+
|
| 1483 |
+
mi_styler.hide(names=True, axis="columns")
|
| 1484 |
+
ctx = mi_styler._translate(True, True)
|
| 1485 |
+
assert ctx["head"][0][1]["display_value"] == " "
|
| 1486 |
+
assert ctx["head"][1][1]["display_value"] == " "
|
| 1487 |
+
|
| 1488 |
+
mi_styler.hide(level=0, axis="columns")
|
| 1489 |
+
ctx = mi_styler._translate(True, True)
|
| 1490 |
+
assert len(ctx["head"]) == 1 # no index names and only one visible column headers
|
| 1491 |
+
assert ctx["head"][0][1]["display_value"] == " "
|
| 1492 |
+
|
| 1493 |
+
|
| 1494 |
+
@pytest.mark.parametrize("caption", [1, ("a", "b", "c"), (1, "s")])
|
| 1495 |
+
def test_caption_raises(mi_styler, caption):
|
| 1496 |
+
msg = "`caption` must be either a string or 2-tuple of strings."
|
| 1497 |
+
with pytest.raises(ValueError, match=msg):
|
| 1498 |
+
mi_styler.set_caption(caption)
|
| 1499 |
+
|
| 1500 |
+
|
| 1501 |
+
def test_hiding_headers_over_index_no_sparsify():
|
| 1502 |
+
# GH 43464
|
| 1503 |
+
midx = MultiIndex.from_product([[1, 2], ["a", "a", "b"]])
|
| 1504 |
+
df = DataFrame(9, index=midx, columns=[0])
|
| 1505 |
+
ctx = df.style._translate(False, False)
|
| 1506 |
+
assert len(ctx["body"]) == 6
|
| 1507 |
+
ctx = df.style.hide((1, "a"), axis=0)._translate(False, False)
|
| 1508 |
+
assert len(ctx["body"]) == 4
|
| 1509 |
+
assert "row2" in ctx["body"][0][0]["class"]
|
| 1510 |
+
|
| 1511 |
+
|
| 1512 |
+
def test_hiding_headers_over_columns_no_sparsify():
|
| 1513 |
+
# GH 43464
|
| 1514 |
+
midx = MultiIndex.from_product([[1, 2], ["a", "a", "b"]])
|
| 1515 |
+
df = DataFrame(9, columns=midx, index=[0])
|
| 1516 |
+
ctx = df.style._translate(False, False)
|
| 1517 |
+
for ix in [(0, 1), (0, 2), (1, 1), (1, 2)]:
|
| 1518 |
+
assert ctx["head"][ix[0]][ix[1]]["is_visible"] is True
|
| 1519 |
+
ctx = df.style.hide((1, "a"), axis="columns")._translate(False, False)
|
| 1520 |
+
for ix in [(0, 1), (0, 2), (1, 1), (1, 2)]:
|
| 1521 |
+
assert ctx["head"][ix[0]][ix[1]]["is_visible"] is False
|
| 1522 |
+
|
| 1523 |
+
|
| 1524 |
+
def test_get_level_lengths_mi_hidden():
|
| 1525 |
+
# GH 43464
|
| 1526 |
+
index = MultiIndex.from_arrays([[1, 1, 1, 2, 2, 2], ["a", "a", "b", "a", "a", "b"]])
|
| 1527 |
+
expected = {
|
| 1528 |
+
(0, 2): 1,
|
| 1529 |
+
(0, 3): 1,
|
| 1530 |
+
(0, 4): 1,
|
| 1531 |
+
(0, 5): 1,
|
| 1532 |
+
(1, 2): 1,
|
| 1533 |
+
(1, 3): 1,
|
| 1534 |
+
(1, 4): 1,
|
| 1535 |
+
(1, 5): 1,
|
| 1536 |
+
}
|
| 1537 |
+
result = _get_level_lengths(
|
| 1538 |
+
index,
|
| 1539 |
+
sparsify=False,
|
| 1540 |
+
max_index=100,
|
| 1541 |
+
hidden_elements=[0, 1, 0, 1], # hidden element can repeat if duplicated index
|
| 1542 |
+
)
|
| 1543 |
+
tm.assert_dict_equal(result, expected)
|
| 1544 |
+
|
| 1545 |
+
|
| 1546 |
+
def test_row_trimming_hide_index():
|
| 1547 |
+
# gh 43703
|
| 1548 |
+
df = DataFrame([[1], [2], [3], [4], [5]])
|
| 1549 |
+
with option_context("styler.render.max_rows", 2):
|
| 1550 |
+
ctx = df.style.hide([0, 1], axis="index")._translate(True, True)
|
| 1551 |
+
assert len(ctx["body"]) == 3
|
| 1552 |
+
for r, val in enumerate(["3", "4", "..."]):
|
| 1553 |
+
assert ctx["body"][r][1]["display_value"] == val
|
| 1554 |
+
|
| 1555 |
+
|
| 1556 |
+
def test_row_trimming_hide_index_mi():
|
| 1557 |
+
# gh 44247
|
| 1558 |
+
df = DataFrame([[1], [2], [3], [4], [5]])
|
| 1559 |
+
df.index = MultiIndex.from_product([[0], [0, 1, 2, 3, 4]])
|
| 1560 |
+
with option_context("styler.render.max_rows", 2):
|
| 1561 |
+
ctx = df.style.hide([(0, 0), (0, 1)], axis="index")._translate(True, True)
|
| 1562 |
+
assert len(ctx["body"]) == 3
|
| 1563 |
+
|
| 1564 |
+
# level 0 index headers (sparsified)
|
| 1565 |
+
assert {"value": 0, "attributes": 'rowspan="2"', "is_visible": True}.items() <= ctx[
|
| 1566 |
+
"body"
|
| 1567 |
+
][0][0].items()
|
| 1568 |
+
assert {"value": 0, "attributes": "", "is_visible": False}.items() <= ctx["body"][
|
| 1569 |
+
1
|
| 1570 |
+
][0].items()
|
| 1571 |
+
assert {"value": "...", "is_visible": True}.items() <= ctx["body"][2][0].items()
|
| 1572 |
+
|
| 1573 |
+
for r, val in enumerate(["2", "3", "..."]):
|
| 1574 |
+
assert ctx["body"][r][1]["display_value"] == val # level 1 index headers
|
| 1575 |
+
for r, val in enumerate(["3", "4", "..."]):
|
| 1576 |
+
assert ctx["body"][r][2]["display_value"] == val # data values
|
| 1577 |
+
|
| 1578 |
+
|
| 1579 |
+
def test_col_trimming_hide_columns():
|
| 1580 |
+
# gh 44272
|
| 1581 |
+
df = DataFrame([[1, 2, 3, 4, 5]])
|
| 1582 |
+
with option_context("styler.render.max_columns", 2):
|
| 1583 |
+
ctx = df.style.hide([0, 1], axis="columns")._translate(True, True)
|
| 1584 |
+
|
| 1585 |
+
assert len(ctx["head"][0]) == 6 # blank, [0, 1 (hidden)], [2 ,3 (visible)], + trim
|
| 1586 |
+
for c, vals in enumerate([(1, False), (2, True), (3, True), ("...", True)]):
|
| 1587 |
+
assert ctx["head"][0][c + 2]["value"] == vals[0]
|
| 1588 |
+
assert ctx["head"][0][c + 2]["is_visible"] == vals[1]
|
| 1589 |
+
|
| 1590 |
+
assert len(ctx["body"][0]) == 6 # index + 2 hidden + 2 visible + trimming col
|
| 1591 |
+
|
| 1592 |
+
|
| 1593 |
+
def test_no_empty_apply(mi_styler):
|
| 1594 |
+
# 45313
|
| 1595 |
+
mi_styler.apply(lambda s: ["a:v;"] * 2, subset=[False, False])
|
| 1596 |
+
mi_styler._compute()
|
| 1597 |
+
|
| 1598 |
+
|
| 1599 |
+
@pytest.mark.parametrize("format", ["html", "latex", "string"])
|
| 1600 |
+
def test_output_buffer(mi_styler, format, temp_file):
|
| 1601 |
+
# gh 47053
|
| 1602 |
+
getattr(mi_styler, f"to_{format}")(temp_file)
|
.venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_to_latex.py
ADDED
|
@@ -0,0 +1,1091 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from textwrap import dedent
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
import pytest
|
| 5 |
+
|
| 6 |
+
from pandas import (
|
| 7 |
+
DataFrame,
|
| 8 |
+
MultiIndex,
|
| 9 |
+
Series,
|
| 10 |
+
option_context,
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
pytest.importorskip("jinja2")
|
| 14 |
+
from pandas.io.formats.style import Styler
|
| 15 |
+
from pandas.io.formats.style_render import (
|
| 16 |
+
_parse_latex_cell_styles,
|
| 17 |
+
_parse_latex_css_conversion,
|
| 18 |
+
_parse_latex_header_span,
|
| 19 |
+
_parse_latex_table_styles,
|
| 20 |
+
_parse_latex_table_wrapping,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@pytest.fixture
|
| 25 |
+
def df():
|
| 26 |
+
return DataFrame(
|
| 27 |
+
{"A": [0, 1], "B": [-0.61, -1.22], "C": Series(["ab", "cd"], dtype=object)}
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@pytest.fixture
|
| 32 |
+
def df_ext():
|
| 33 |
+
return DataFrame(
|
| 34 |
+
{"A": [0, 1, 2], "B": [-0.61, -1.22, -2.22], "C": ["ab", "cd", "de"]}
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@pytest.fixture
|
| 39 |
+
def styler(df):
|
| 40 |
+
return Styler(df, uuid_len=0, precision=2)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def test_minimal_latex_tabular(styler):
|
| 44 |
+
expected = dedent(
|
| 45 |
+
"""\
|
| 46 |
+
\\begin{tabular}{lrrl}
|
| 47 |
+
& A & B & C \\\\
|
| 48 |
+
0 & 0 & -0.61 & ab \\\\
|
| 49 |
+
1 & 1 & -1.22 & cd \\\\
|
| 50 |
+
\\end{tabular}
|
| 51 |
+
"""
|
| 52 |
+
)
|
| 53 |
+
assert styler.to_latex() == expected
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def test_tabular_hrules(styler):
|
| 57 |
+
expected = dedent(
|
| 58 |
+
"""\
|
| 59 |
+
\\begin{tabular}{lrrl}
|
| 60 |
+
\\toprule
|
| 61 |
+
& A & B & C \\\\
|
| 62 |
+
\\midrule
|
| 63 |
+
0 & 0 & -0.61 & ab \\\\
|
| 64 |
+
1 & 1 & -1.22 & cd \\\\
|
| 65 |
+
\\bottomrule
|
| 66 |
+
\\end{tabular}
|
| 67 |
+
"""
|
| 68 |
+
)
|
| 69 |
+
assert styler.to_latex(hrules=True) == expected
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def test_tabular_custom_hrules(styler):
|
| 73 |
+
styler.set_table_styles(
|
| 74 |
+
[
|
| 75 |
+
{"selector": "toprule", "props": ":hline"},
|
| 76 |
+
{"selector": "bottomrule", "props": ":otherline"},
|
| 77 |
+
]
|
| 78 |
+
) # no midrule
|
| 79 |
+
expected = dedent(
|
| 80 |
+
"""\
|
| 81 |
+
\\begin{tabular}{lrrl}
|
| 82 |
+
\\hline
|
| 83 |
+
& A & B & C \\\\
|
| 84 |
+
0 & 0 & -0.61 & ab \\\\
|
| 85 |
+
1 & 1 & -1.22 & cd \\\\
|
| 86 |
+
\\otherline
|
| 87 |
+
\\end{tabular}
|
| 88 |
+
"""
|
| 89 |
+
)
|
| 90 |
+
assert styler.to_latex() == expected
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def test_column_format(styler):
|
| 94 |
+
# default setting is already tested in `test_latex_minimal_tabular`
|
| 95 |
+
styler.set_table_styles([{"selector": "column_format", "props": ":cccc"}])
|
| 96 |
+
|
| 97 |
+
assert "\\begin{tabular}{rrrr}" in styler.to_latex(column_format="rrrr")
|
| 98 |
+
styler.set_table_styles([{"selector": "column_format", "props": ":r|r|cc"}])
|
| 99 |
+
assert "\\begin{tabular}{r|r|cc}" in styler.to_latex()
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def test_siunitx_cols(styler):
|
| 103 |
+
expected = dedent(
|
| 104 |
+
"""\
|
| 105 |
+
\\begin{tabular}{lSSl}
|
| 106 |
+
{} & {A} & {B} & {C} \\\\
|
| 107 |
+
0 & 0 & -0.61 & ab \\\\
|
| 108 |
+
1 & 1 & -1.22 & cd \\\\
|
| 109 |
+
\\end{tabular}
|
| 110 |
+
"""
|
| 111 |
+
)
|
| 112 |
+
assert styler.to_latex(siunitx=True) == expected
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def test_position(styler):
|
| 116 |
+
assert "\\begin{table}[h!]" in styler.to_latex(position="h!")
|
| 117 |
+
assert "\\end{table}" in styler.to_latex(position="h!")
|
| 118 |
+
styler.set_table_styles([{"selector": "position", "props": ":b!"}])
|
| 119 |
+
assert "\\begin{table}[b!]" in styler.to_latex()
|
| 120 |
+
assert "\\end{table}" in styler.to_latex()
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
@pytest.mark.parametrize("env", [None, "longtable"])
|
| 124 |
+
def test_label(styler, env):
|
| 125 |
+
assert "\n\\label{text}" in styler.to_latex(label="text", environment=env)
|
| 126 |
+
styler.set_table_styles([{"selector": "label", "props": ":{more §text}"}])
|
| 127 |
+
assert "\n\\label{more :text}" in styler.to_latex(environment=env)
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def test_position_float_raises(styler):
|
| 131 |
+
msg = "`position_float` should be one of 'raggedright', 'raggedleft', 'centering',"
|
| 132 |
+
with pytest.raises(ValueError, match=msg):
|
| 133 |
+
styler.to_latex(position_float="bad_string")
|
| 134 |
+
|
| 135 |
+
msg = "`position_float` cannot be used in 'longtable' `environment`"
|
| 136 |
+
with pytest.raises(ValueError, match=msg):
|
| 137 |
+
styler.to_latex(position_float="centering", environment="longtable")
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
@pytest.mark.parametrize("label", [(None, ""), ("text", "\\label{text}")])
|
| 141 |
+
@pytest.mark.parametrize("position", [(None, ""), ("h!", "{table}[h!]")])
|
| 142 |
+
@pytest.mark.parametrize("caption", [(None, ""), ("text", "\\caption{text}")])
|
| 143 |
+
@pytest.mark.parametrize("column_format", [(None, ""), ("rcrl", "{tabular}{rcrl}")])
|
| 144 |
+
@pytest.mark.parametrize("position_float", [(None, ""), ("centering", "\\centering")])
|
| 145 |
+
def test_kwargs_combinations(
|
| 146 |
+
styler, label, position, caption, column_format, position_float
|
| 147 |
+
):
|
| 148 |
+
result = styler.to_latex(
|
| 149 |
+
label=label[0],
|
| 150 |
+
position=position[0],
|
| 151 |
+
caption=caption[0],
|
| 152 |
+
column_format=column_format[0],
|
| 153 |
+
position_float=position_float[0],
|
| 154 |
+
)
|
| 155 |
+
assert label[1] in result
|
| 156 |
+
assert position[1] in result
|
| 157 |
+
assert caption[1] in result
|
| 158 |
+
assert column_format[1] in result
|
| 159 |
+
assert position_float[1] in result
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def test_custom_table_styles(styler):
|
| 163 |
+
styler.set_table_styles(
|
| 164 |
+
[
|
| 165 |
+
{"selector": "mycommand", "props": ":{myoptions}"},
|
| 166 |
+
{"selector": "mycommand2", "props": ":{myoptions2}"},
|
| 167 |
+
]
|
| 168 |
+
)
|
| 169 |
+
expected = dedent(
|
| 170 |
+
"""\
|
| 171 |
+
\\begin{table}
|
| 172 |
+
\\mycommand{myoptions}
|
| 173 |
+
\\mycommand2{myoptions2}
|
| 174 |
+
"""
|
| 175 |
+
)
|
| 176 |
+
assert expected in styler.to_latex()
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def test_cell_styling(styler):
|
| 180 |
+
styler.highlight_max(props="itshape:;Huge:--wrap;")
|
| 181 |
+
expected = dedent(
|
| 182 |
+
"""\
|
| 183 |
+
\\begin{tabular}{lrrl}
|
| 184 |
+
& A & B & C \\\\
|
| 185 |
+
0 & 0 & \\itshape {\\Huge -0.61} & ab \\\\
|
| 186 |
+
1 & \\itshape {\\Huge 1} & -1.22 & \\itshape {\\Huge cd} \\\\
|
| 187 |
+
\\end{tabular}
|
| 188 |
+
"""
|
| 189 |
+
)
|
| 190 |
+
assert expected == styler.to_latex()
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def test_multiindex_columns(df):
|
| 194 |
+
cidx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")])
|
| 195 |
+
df.columns = cidx
|
| 196 |
+
expected = dedent(
|
| 197 |
+
"""\
|
| 198 |
+
\\begin{tabular}{lrrl}
|
| 199 |
+
& \\multicolumn{2}{r}{A} & B \\\\
|
| 200 |
+
& a & b & c \\\\
|
| 201 |
+
0 & 0 & -0.61 & ab \\\\
|
| 202 |
+
1 & 1 & -1.22 & cd \\\\
|
| 203 |
+
\\end{tabular}
|
| 204 |
+
"""
|
| 205 |
+
)
|
| 206 |
+
s = df.style.format(precision=2)
|
| 207 |
+
assert expected == s.to_latex()
|
| 208 |
+
|
| 209 |
+
# non-sparse
|
| 210 |
+
expected = dedent(
|
| 211 |
+
"""\
|
| 212 |
+
\\begin{tabular}{lrrl}
|
| 213 |
+
& A & A & B \\\\
|
| 214 |
+
& a & b & c \\\\
|
| 215 |
+
0 & 0 & -0.61 & ab \\\\
|
| 216 |
+
1 & 1 & -1.22 & cd \\\\
|
| 217 |
+
\\end{tabular}
|
| 218 |
+
"""
|
| 219 |
+
)
|
| 220 |
+
s = df.style.format(precision=2)
|
| 221 |
+
assert expected == s.to_latex(sparse_columns=False)
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def test_multiindex_row(df_ext):
|
| 225 |
+
ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")])
|
| 226 |
+
df_ext.index = ridx
|
| 227 |
+
expected = dedent(
|
| 228 |
+
"""\
|
| 229 |
+
\\begin{tabular}{llrrl}
|
| 230 |
+
& & A & B & C \\\\
|
| 231 |
+
\\multirow[c]{2}{*}{A} & a & 0 & -0.61 & ab \\\\
|
| 232 |
+
& b & 1 & -1.22 & cd \\\\
|
| 233 |
+
B & c & 2 & -2.22 & de \\\\
|
| 234 |
+
\\end{tabular}
|
| 235 |
+
"""
|
| 236 |
+
)
|
| 237 |
+
styler = df_ext.style.format(precision=2)
|
| 238 |
+
result = styler.to_latex()
|
| 239 |
+
assert expected == result
|
| 240 |
+
|
| 241 |
+
# non-sparse
|
| 242 |
+
expected = dedent(
|
| 243 |
+
"""\
|
| 244 |
+
\\begin{tabular}{llrrl}
|
| 245 |
+
& & A & B & C \\\\
|
| 246 |
+
A & a & 0 & -0.61 & ab \\\\
|
| 247 |
+
A & b & 1 & -1.22 & cd \\\\
|
| 248 |
+
B & c & 2 & -2.22 & de \\\\
|
| 249 |
+
\\end{tabular}
|
| 250 |
+
"""
|
| 251 |
+
)
|
| 252 |
+
result = styler.to_latex(sparse_index=False)
|
| 253 |
+
assert expected == result
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
def test_multirow_naive(df_ext):
|
| 257 |
+
ridx = MultiIndex.from_tuples([("X", "x"), ("X", "y"), ("Y", "z")])
|
| 258 |
+
df_ext.index = ridx
|
| 259 |
+
expected = dedent(
|
| 260 |
+
"""\
|
| 261 |
+
\\begin{tabular}{llrrl}
|
| 262 |
+
& & A & B & C \\\\
|
| 263 |
+
X & x & 0 & -0.61 & ab \\\\
|
| 264 |
+
& y & 1 & -1.22 & cd \\\\
|
| 265 |
+
Y & z & 2 & -2.22 & de \\\\
|
| 266 |
+
\\end{tabular}
|
| 267 |
+
"""
|
| 268 |
+
)
|
| 269 |
+
styler = df_ext.style.format(precision=2)
|
| 270 |
+
result = styler.to_latex(multirow_align="naive")
|
| 271 |
+
assert expected == result
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
def test_multiindex_row_and_col(df_ext):
|
| 275 |
+
cidx = MultiIndex.from_tuples([("Z", "a"), ("Z", "b"), ("Y", "c")])
|
| 276 |
+
ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")])
|
| 277 |
+
df_ext.index, df_ext.columns = ridx, cidx
|
| 278 |
+
expected = dedent(
|
| 279 |
+
"""\
|
| 280 |
+
\\begin{tabular}{llrrl}
|
| 281 |
+
& & \\multicolumn{2}{l}{Z} & Y \\\\
|
| 282 |
+
& & a & b & c \\\\
|
| 283 |
+
\\multirow[b]{2}{*}{A} & a & 0 & -0.61 & ab \\\\
|
| 284 |
+
& b & 1 & -1.22 & cd \\\\
|
| 285 |
+
B & c & 2 & -2.22 & de \\\\
|
| 286 |
+
\\end{tabular}
|
| 287 |
+
"""
|
| 288 |
+
)
|
| 289 |
+
styler = df_ext.style.format(precision=2)
|
| 290 |
+
result = styler.to_latex(multirow_align="b", multicol_align="l")
|
| 291 |
+
assert result == expected
|
| 292 |
+
|
| 293 |
+
# non-sparse
|
| 294 |
+
expected = dedent(
|
| 295 |
+
"""\
|
| 296 |
+
\\begin{tabular}{llrrl}
|
| 297 |
+
& & Z & Z & Y \\\\
|
| 298 |
+
& & a & b & c \\\\
|
| 299 |
+
A & a & 0 & -0.61 & ab \\\\
|
| 300 |
+
A & b & 1 & -1.22 & cd \\\\
|
| 301 |
+
B & c & 2 & -2.22 & de \\\\
|
| 302 |
+
\\end{tabular}
|
| 303 |
+
"""
|
| 304 |
+
)
|
| 305 |
+
result = styler.to_latex(sparse_index=False, sparse_columns=False)
|
| 306 |
+
assert result == expected
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
@pytest.mark.parametrize(
|
| 310 |
+
"multicol_align, siunitx, header",
|
| 311 |
+
[
|
| 312 |
+
("naive-l", False, " & A & &"),
|
| 313 |
+
("naive-r", False, " & & & A"),
|
| 314 |
+
("naive-l", True, "{} & {A} & {} & {}"),
|
| 315 |
+
("naive-r", True, "{} & {} & {} & {A}"),
|
| 316 |
+
],
|
| 317 |
+
)
|
| 318 |
+
def test_multicol_naive(df, multicol_align, siunitx, header):
|
| 319 |
+
ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("A", "c")])
|
| 320 |
+
df.columns = ridx
|
| 321 |
+
level1 = " & a & b & c" if not siunitx else "{} & {a} & {b} & {c}"
|
| 322 |
+
col_format = "lrrl" if not siunitx else "lSSl"
|
| 323 |
+
expected = dedent(
|
| 324 |
+
f"""\
|
| 325 |
+
\\begin{{tabular}}{{{col_format}}}
|
| 326 |
+
{header} \\\\
|
| 327 |
+
{level1} \\\\
|
| 328 |
+
0 & 0 & -0.61 & ab \\\\
|
| 329 |
+
1 & 1 & -1.22 & cd \\\\
|
| 330 |
+
\\end{{tabular}}
|
| 331 |
+
"""
|
| 332 |
+
)
|
| 333 |
+
styler = df.style.format(precision=2)
|
| 334 |
+
result = styler.to_latex(multicol_align=multicol_align, siunitx=siunitx)
|
| 335 |
+
assert expected == result
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
def test_multi_options(df_ext):
|
| 339 |
+
cidx = MultiIndex.from_tuples([("Z", "a"), ("Z", "b"), ("Y", "c")])
|
| 340 |
+
ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")])
|
| 341 |
+
df_ext.index, df_ext.columns = ridx, cidx
|
| 342 |
+
styler = df_ext.style.format(precision=2)
|
| 343 |
+
|
| 344 |
+
expected = dedent(
|
| 345 |
+
"""\
|
| 346 |
+
& & \\multicolumn{2}{r}{Z} & Y \\\\
|
| 347 |
+
& & a & b & c \\\\
|
| 348 |
+
\\multirow[c]{2}{*}{A} & a & 0 & -0.61 & ab \\\\
|
| 349 |
+
"""
|
| 350 |
+
)
|
| 351 |
+
result = styler.to_latex()
|
| 352 |
+
assert expected in result
|
| 353 |
+
|
| 354 |
+
with option_context("styler.latex.multicol_align", "l"):
|
| 355 |
+
assert " & & \\multicolumn{2}{l}{Z} & Y \\\\" in styler.to_latex()
|
| 356 |
+
|
| 357 |
+
with option_context("styler.latex.multirow_align", "b"):
|
| 358 |
+
assert "\\multirow[b]{2}{*}{A} & a & 0 & -0.61 & ab \\\\" in styler.to_latex()
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
def test_multiindex_columns_hidden():
|
| 362 |
+
df = DataFrame([[1, 2, 3, 4]])
|
| 363 |
+
df.columns = MultiIndex.from_tuples([("A", 1), ("A", 2), ("A", 3), ("B", 1)])
|
| 364 |
+
s = df.style
|
| 365 |
+
assert "{tabular}{lrrrr}" in s.to_latex()
|
| 366 |
+
s.set_table_styles([]) # reset the position command
|
| 367 |
+
s.hide([("A", 2)], axis="columns")
|
| 368 |
+
assert "{tabular}{lrrr}" in s.to_latex()
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
@pytest.mark.parametrize(
|
| 372 |
+
"option, value",
|
| 373 |
+
[
|
| 374 |
+
("styler.sparse.index", True),
|
| 375 |
+
("styler.sparse.index", False),
|
| 376 |
+
("styler.sparse.columns", True),
|
| 377 |
+
("styler.sparse.columns", False),
|
| 378 |
+
],
|
| 379 |
+
)
|
| 380 |
+
def test_sparse_options(df_ext, option, value):
|
| 381 |
+
cidx = MultiIndex.from_tuples([("Z", "a"), ("Z", "b"), ("Y", "c")])
|
| 382 |
+
ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")])
|
| 383 |
+
df_ext.index, df_ext.columns = ridx, cidx
|
| 384 |
+
styler = df_ext.style
|
| 385 |
+
|
| 386 |
+
latex1 = styler.to_latex()
|
| 387 |
+
with option_context(option, value):
|
| 388 |
+
latex2 = styler.to_latex()
|
| 389 |
+
assert (latex1 == latex2) is value
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
def test_hidden_index(styler):
|
| 393 |
+
styler.hide(axis="index")
|
| 394 |
+
expected = dedent(
|
| 395 |
+
"""\
|
| 396 |
+
\\begin{tabular}{rrl}
|
| 397 |
+
A & B & C \\\\
|
| 398 |
+
0 & -0.61 & ab \\\\
|
| 399 |
+
1 & -1.22 & cd \\\\
|
| 400 |
+
\\end{tabular}
|
| 401 |
+
"""
|
| 402 |
+
)
|
| 403 |
+
assert styler.to_latex() == expected
|
| 404 |
+
|
| 405 |
+
|
| 406 |
+
@pytest.mark.parametrize("environment", ["table", "figure*", None])
|
| 407 |
+
def test_comprehensive(df_ext, environment):
|
| 408 |
+
# test as many low level features simultaneously as possible
|
| 409 |
+
cidx = MultiIndex.from_tuples([("Z", "a"), ("Z", "b"), ("Y", "c")])
|
| 410 |
+
ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")])
|
| 411 |
+
df_ext.index, df_ext.columns = ridx, cidx
|
| 412 |
+
stlr = df_ext.style
|
| 413 |
+
stlr.set_caption("mycap")
|
| 414 |
+
stlr.set_table_styles(
|
| 415 |
+
[
|
| 416 |
+
{"selector": "label", "props": ":{fig§item}"},
|
| 417 |
+
{"selector": "position", "props": ":h!"},
|
| 418 |
+
{"selector": "position_float", "props": ":centering"},
|
| 419 |
+
{"selector": "column_format", "props": ":rlrlr"},
|
| 420 |
+
{"selector": "toprule", "props": ":toprule"},
|
| 421 |
+
{"selector": "midrule", "props": ":midrule"},
|
| 422 |
+
{"selector": "bottomrule", "props": ":bottomrule"},
|
| 423 |
+
{"selector": "rowcolors", "props": ":{3}{pink}{}"}, # custom command
|
| 424 |
+
]
|
| 425 |
+
)
|
| 426 |
+
stlr.highlight_max(axis=0, props="textbf:--rwrap;cellcolor:[rgb]{1,1,0.6}--rwrap")
|
| 427 |
+
stlr.highlight_max(axis=None, props="Huge:--wrap;", subset=[("Z", "a"), ("Z", "b")])
|
| 428 |
+
|
| 429 |
+
expected = (
|
| 430 |
+
"""\
|
| 431 |
+
\\begin{table}[h!]
|
| 432 |
+
\\centering
|
| 433 |
+
\\caption{mycap}
|
| 434 |
+
\\label{fig:item}
|
| 435 |
+
\\rowcolors{3}{pink}{}
|
| 436 |
+
\\begin{tabular}{rlrlr}
|
| 437 |
+
\\toprule
|
| 438 |
+
& & \\multicolumn{2}{r}{Z} & Y \\\\
|
| 439 |
+
& & a & b & c \\\\
|
| 440 |
+
\\midrule
|
| 441 |
+
\\multirow[c]{2}{*}{A} & a & 0 & \\textbf{\\cellcolor[rgb]{1,1,0.6}{-0.61}} & ab \\\\
|
| 442 |
+
& b & 1 & -1.22 & cd \\\\
|
| 443 |
+
B & c & \\textbf{\\cellcolor[rgb]{1,1,0.6}{{\\Huge 2}}} & -2.22 & """
|
| 444 |
+
"""\
|
| 445 |
+
\\textbf{\\cellcolor[rgb]{1,1,0.6}{de}} \\\\
|
| 446 |
+
\\bottomrule
|
| 447 |
+
\\end{tabular}
|
| 448 |
+
\\end{table}
|
| 449 |
+
"""
|
| 450 |
+
).replace("table", environment if environment else "table")
|
| 451 |
+
result = stlr.format(precision=2).to_latex(environment=environment)
|
| 452 |
+
assert result == expected
|
| 453 |
+
|
| 454 |
+
|
| 455 |
+
def test_environment_option(styler):
|
| 456 |
+
with option_context("styler.latex.environment", "bar-env"):
|
| 457 |
+
assert "\\begin{bar-env}" in styler.to_latex()
|
| 458 |
+
assert "\\begin{foo-env}" in styler.to_latex(environment="foo-env")
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
def test_parse_latex_table_styles(styler):
|
| 462 |
+
styler.set_table_styles(
|
| 463 |
+
[
|
| 464 |
+
{"selector": "foo", "props": [("attr", "value")]},
|
| 465 |
+
{"selector": "bar", "props": [("attr", "overwritten")]},
|
| 466 |
+
{"selector": "bar", "props": [("attr", "baz"), ("attr2", "ignored")]},
|
| 467 |
+
{"selector": "label", "props": [("", "{fig§item}")]},
|
| 468 |
+
]
|
| 469 |
+
)
|
| 470 |
+
assert _parse_latex_table_styles(styler.table_styles, "bar") == "baz"
|
| 471 |
+
|
| 472 |
+
# test '§' replaced by ':' [for CSS compatibility]
|
| 473 |
+
assert _parse_latex_table_styles(styler.table_styles, "label") == "{fig:item}"
|
| 474 |
+
|
| 475 |
+
|
| 476 |
+
def test_parse_latex_cell_styles_basic(): # test nesting
|
| 477 |
+
cell_style = [("itshape", "--rwrap"), ("cellcolor", "[rgb]{0,1,1}--rwrap")]
|
| 478 |
+
expected = "\\itshape{\\cellcolor[rgb]{0,1,1}{text}}"
|
| 479 |
+
assert _parse_latex_cell_styles(cell_style, "text") == expected
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
@pytest.mark.parametrize(
|
| 483 |
+
"wrap_arg, expected",
|
| 484 |
+
[ # test wrapping
|
| 485 |
+
("", "\\<command><options> <display_value>"),
|
| 486 |
+
("--wrap", "{\\<command><options> <display_value>}"),
|
| 487 |
+
("--nowrap", "\\<command><options> <display_value>"),
|
| 488 |
+
("--lwrap", "{\\<command><options>} <display_value>"),
|
| 489 |
+
("--dwrap", "{\\<command><options>}{<display_value>}"),
|
| 490 |
+
("--rwrap", "\\<command><options>{<display_value>}"),
|
| 491 |
+
],
|
| 492 |
+
)
|
| 493 |
+
def test_parse_latex_cell_styles_braces(wrap_arg, expected):
|
| 494 |
+
cell_style = [("<command>", f"<options>{wrap_arg}")]
|
| 495 |
+
assert _parse_latex_cell_styles(cell_style, "<display_value>") == expected
|
| 496 |
+
|
| 497 |
+
|
| 498 |
+
def test_parse_latex_header_span():
|
| 499 |
+
cell = {"attributes": 'colspan="3"', "display_value": "text", "cellstyle": []}
|
| 500 |
+
expected = "\\multicolumn{3}{Y}{text}"
|
| 501 |
+
assert _parse_latex_header_span(cell, "X", "Y") == expected
|
| 502 |
+
|
| 503 |
+
cell = {"attributes": 'rowspan="5"', "display_value": "text", "cellstyle": []}
|
| 504 |
+
expected = "\\multirow[X]{5}{*}{text}"
|
| 505 |
+
assert _parse_latex_header_span(cell, "X", "Y") == expected
|
| 506 |
+
|
| 507 |
+
cell = {"display_value": "text", "cellstyle": []}
|
| 508 |
+
assert _parse_latex_header_span(cell, "X", "Y") == "text"
|
| 509 |
+
|
| 510 |
+
cell = {"display_value": "text", "cellstyle": [("bfseries", "--rwrap")]}
|
| 511 |
+
assert _parse_latex_header_span(cell, "X", "Y") == "\\bfseries{text}"
|
| 512 |
+
|
| 513 |
+
|
| 514 |
+
def test_parse_latex_table_wrapping(styler):
|
| 515 |
+
styler.set_table_styles(
|
| 516 |
+
[
|
| 517 |
+
{"selector": "toprule", "props": ":value"},
|
| 518 |
+
{"selector": "bottomrule", "props": ":value"},
|
| 519 |
+
{"selector": "midrule", "props": ":value"},
|
| 520 |
+
{"selector": "column_format", "props": ":value"},
|
| 521 |
+
]
|
| 522 |
+
)
|
| 523 |
+
assert _parse_latex_table_wrapping(styler.table_styles, styler.caption) is False
|
| 524 |
+
assert _parse_latex_table_wrapping(styler.table_styles, "some caption") is True
|
| 525 |
+
styler.set_table_styles(
|
| 526 |
+
[
|
| 527 |
+
{"selector": "not-ignored", "props": ":value"},
|
| 528 |
+
],
|
| 529 |
+
overwrite=False,
|
| 530 |
+
)
|
| 531 |
+
assert _parse_latex_table_wrapping(styler.table_styles, None) is True
|
| 532 |
+
|
| 533 |
+
|
| 534 |
+
def test_short_caption(styler):
|
| 535 |
+
result = styler.to_latex(caption=("full cap", "short cap"))
|
| 536 |
+
assert "\\caption[short cap]{full cap}" in result
|
| 537 |
+
|
| 538 |
+
|
| 539 |
+
@pytest.mark.parametrize(
|
| 540 |
+
"css, expected",
|
| 541 |
+
[
|
| 542 |
+
([("color", "red")], [("color", "{red}")]), # test color and input format types
|
| 543 |
+
(
|
| 544 |
+
[("color", "rgb(128, 128, 128 )")],
|
| 545 |
+
[("color", "[rgb]{0.502, 0.502, 0.502}")],
|
| 546 |
+
),
|
| 547 |
+
(
|
| 548 |
+
[("color", "rgb(128, 50%, 25% )")],
|
| 549 |
+
[("color", "[rgb]{0.502, 0.500, 0.250}")],
|
| 550 |
+
),
|
| 551 |
+
(
|
| 552 |
+
[("color", "rgba(128,128,128,1)")],
|
| 553 |
+
[("color", "[rgb]{0.502, 0.502, 0.502}")],
|
| 554 |
+
),
|
| 555 |
+
([("color", "#FF00FF")], [("color", "[HTML]{FF00FF}")]),
|
| 556 |
+
([("color", "#F0F")], [("color", "[HTML]{FF00FF}")]),
|
| 557 |
+
([("font-weight", "bold")], [("bfseries", "")]), # test font-weight and types
|
| 558 |
+
([("font-weight", "bolder")], [("bfseries", "")]),
|
| 559 |
+
([("font-weight", "normal")], []),
|
| 560 |
+
([("background-color", "red")], [("cellcolor", "{red}--lwrap")]),
|
| 561 |
+
(
|
| 562 |
+
[("background-color", "#FF00FF")], # test background-color command and wrap
|
| 563 |
+
[("cellcolor", "[HTML]{FF00FF}--lwrap")],
|
| 564 |
+
),
|
| 565 |
+
([("font-style", "italic")], [("itshape", "")]), # test font-style and types
|
| 566 |
+
([("font-style", "oblique")], [("slshape", "")]),
|
| 567 |
+
([("font-style", "normal")], []),
|
| 568 |
+
([("color", "red /*--dwrap*/")], [("color", "{red}--dwrap")]), # css comments
|
| 569 |
+
([("background-color", "red /* --dwrap */")], [("cellcolor", "{red}--dwrap")]),
|
| 570 |
+
],
|
| 571 |
+
)
|
| 572 |
+
def test_parse_latex_css_conversion(css, expected):
|
| 573 |
+
result = _parse_latex_css_conversion(css)
|
| 574 |
+
assert result == expected
|
| 575 |
+
|
| 576 |
+
|
| 577 |
+
@pytest.mark.parametrize(
|
| 578 |
+
"env, inner_env",
|
| 579 |
+
[
|
| 580 |
+
(None, "tabular"),
|
| 581 |
+
("table", "tabular"),
|
| 582 |
+
("longtable", "longtable"),
|
| 583 |
+
],
|
| 584 |
+
)
|
| 585 |
+
@pytest.mark.parametrize(
|
| 586 |
+
"convert, exp", [(True, "bfseries"), (False, "font-weightbold")]
|
| 587 |
+
)
|
| 588 |
+
def test_parse_latex_css_convert_minimal(styler, env, inner_env, convert, exp):
|
| 589 |
+
# parameters ensure longtable template is also tested
|
| 590 |
+
styler.highlight_max(props="font-weight:bold;")
|
| 591 |
+
result = styler.to_latex(convert_css=convert, environment=env)
|
| 592 |
+
expected = dedent(
|
| 593 |
+
f"""\
|
| 594 |
+
0 & 0 & \\{exp} -0.61 & ab \\\\
|
| 595 |
+
1 & \\{exp} 1 & -1.22 & \\{exp} cd \\\\
|
| 596 |
+
\\end{{{inner_env}}}
|
| 597 |
+
"""
|
| 598 |
+
)
|
| 599 |
+
assert expected in result
|
| 600 |
+
|
| 601 |
+
|
| 602 |
+
def test_parse_latex_css_conversion_option():
|
| 603 |
+
css = [("command", "option--latex--wrap")]
|
| 604 |
+
expected = [("command", "option--wrap")]
|
| 605 |
+
result = _parse_latex_css_conversion(css)
|
| 606 |
+
assert result == expected
|
| 607 |
+
|
| 608 |
+
|
| 609 |
+
def test_styler_object_after_render(styler):
|
| 610 |
+
# GH 42320
|
| 611 |
+
pre_render = styler._copy(deepcopy=True)
|
| 612 |
+
styler.to_latex(
|
| 613 |
+
column_format="rllr",
|
| 614 |
+
position="h",
|
| 615 |
+
position_float="centering",
|
| 616 |
+
hrules=True,
|
| 617 |
+
label="my lab",
|
| 618 |
+
caption="my cap",
|
| 619 |
+
)
|
| 620 |
+
|
| 621 |
+
assert pre_render.table_styles == styler.table_styles
|
| 622 |
+
assert pre_render.caption == styler.caption
|
| 623 |
+
|
| 624 |
+
|
| 625 |
+
def test_longtable_comprehensive(styler):
|
| 626 |
+
result = styler.to_latex(
|
| 627 |
+
environment="longtable", hrules=True, label="fig:A", caption=("full", "short")
|
| 628 |
+
)
|
| 629 |
+
expected = dedent(
|
| 630 |
+
"""\
|
| 631 |
+
\\begin{longtable}{lrrl}
|
| 632 |
+
\\caption[short]{full} \\label{fig:A} \\\\
|
| 633 |
+
\\toprule
|
| 634 |
+
& A & B & C \\\\
|
| 635 |
+
\\midrule
|
| 636 |
+
\\endfirsthead
|
| 637 |
+
\\caption[]{full} \\\\
|
| 638 |
+
\\toprule
|
| 639 |
+
& A & B & C \\\\
|
| 640 |
+
\\midrule
|
| 641 |
+
\\endhead
|
| 642 |
+
\\midrule
|
| 643 |
+
\\multicolumn{4}{r}{Continued on next page} \\\\
|
| 644 |
+
\\midrule
|
| 645 |
+
\\endfoot
|
| 646 |
+
\\bottomrule
|
| 647 |
+
\\endlastfoot
|
| 648 |
+
0 & 0 & -0.61 & ab \\\\
|
| 649 |
+
1 & 1 & -1.22 & cd \\\\
|
| 650 |
+
\\end{longtable}
|
| 651 |
+
"""
|
| 652 |
+
)
|
| 653 |
+
assert result == expected
|
| 654 |
+
|
| 655 |
+
|
| 656 |
+
def test_longtable_minimal(styler):
|
| 657 |
+
result = styler.to_latex(environment="longtable")
|
| 658 |
+
expected = dedent(
|
| 659 |
+
"""\
|
| 660 |
+
\\begin{longtable}{lrrl}
|
| 661 |
+
& A & B & C \\\\
|
| 662 |
+
\\endfirsthead
|
| 663 |
+
& A & B & C \\\\
|
| 664 |
+
\\endhead
|
| 665 |
+
\\multicolumn{4}{r}{Continued on next page} \\\\
|
| 666 |
+
\\endfoot
|
| 667 |
+
\\endlastfoot
|
| 668 |
+
0 & 0 & -0.61 & ab \\\\
|
| 669 |
+
1 & 1 & -1.22 & cd \\\\
|
| 670 |
+
\\end{longtable}
|
| 671 |
+
"""
|
| 672 |
+
)
|
| 673 |
+
assert result == expected
|
| 674 |
+
|
| 675 |
+
|
| 676 |
+
@pytest.mark.parametrize(
|
| 677 |
+
"sparse, exp, siunitx",
|
| 678 |
+
[
|
| 679 |
+
(True, "{} & \\multicolumn{2}{r}{A} & {B}", True),
|
| 680 |
+
(False, "{} & {A} & {A} & {B}", True),
|
| 681 |
+
(True, " & \\multicolumn{2}{r}{A} & B", False),
|
| 682 |
+
(False, " & A & A & B", False),
|
| 683 |
+
],
|
| 684 |
+
)
|
| 685 |
+
def test_longtable_multiindex_columns(df, sparse, exp, siunitx):
|
| 686 |
+
cidx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")])
|
| 687 |
+
df.columns = cidx
|
| 688 |
+
with_si = "{} & {a} & {b} & {c} \\\\"
|
| 689 |
+
without_si = " & a & b & c \\\\"
|
| 690 |
+
expected = dedent(
|
| 691 |
+
f"""\
|
| 692 |
+
\\begin{{longtable}}{{l{"SS" if siunitx else "rr"}l}}
|
| 693 |
+
{exp} \\\\
|
| 694 |
+
{with_si if siunitx else without_si}
|
| 695 |
+
\\endfirsthead
|
| 696 |
+
{exp} \\\\
|
| 697 |
+
{with_si if siunitx else without_si}
|
| 698 |
+
\\endhead
|
| 699 |
+
"""
|
| 700 |
+
)
|
| 701 |
+
result = df.style.to_latex(
|
| 702 |
+
environment="longtable", sparse_columns=sparse, siunitx=siunitx
|
| 703 |
+
)
|
| 704 |
+
assert expected in result
|
| 705 |
+
|
| 706 |
+
|
| 707 |
+
@pytest.mark.parametrize(
|
| 708 |
+
"caption, cap_exp",
|
| 709 |
+
[
|
| 710 |
+
("full", ("{full}", "")),
|
| 711 |
+
(("full", "short"), ("{full}", "[short]")),
|
| 712 |
+
],
|
| 713 |
+
)
|
| 714 |
+
@pytest.mark.parametrize("label, lab_exp", [(None, ""), ("tab:A", " \\label{tab:A}")])
|
| 715 |
+
def test_longtable_caption_label(styler, caption, cap_exp, label, lab_exp):
|
| 716 |
+
cap_exp1 = f"\\caption{cap_exp[1]}{cap_exp[0]}"
|
| 717 |
+
cap_exp2 = f"\\caption[]{cap_exp[0]}"
|
| 718 |
+
|
| 719 |
+
expected = dedent(
|
| 720 |
+
f"""\
|
| 721 |
+
{cap_exp1}{lab_exp} \\\\
|
| 722 |
+
& A & B & C \\\\
|
| 723 |
+
\\endfirsthead
|
| 724 |
+
{cap_exp2} \\\\
|
| 725 |
+
"""
|
| 726 |
+
)
|
| 727 |
+
assert expected in styler.to_latex(
|
| 728 |
+
environment="longtable", caption=caption, label=label
|
| 729 |
+
)
|
| 730 |
+
|
| 731 |
+
|
| 732 |
+
@pytest.mark.parametrize("index", [True, False])
|
| 733 |
+
@pytest.mark.parametrize(
|
| 734 |
+
"columns, siunitx",
|
| 735 |
+
[
|
| 736 |
+
(True, True),
|
| 737 |
+
(True, False),
|
| 738 |
+
(False, False),
|
| 739 |
+
],
|
| 740 |
+
)
|
| 741 |
+
def test_apply_map_header_render_mi(df_ext, index, columns, siunitx):
|
| 742 |
+
cidx = MultiIndex.from_tuples([("Z", "a"), ("Z", "b"), ("Y", "c")])
|
| 743 |
+
ridx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")])
|
| 744 |
+
df_ext.index, df_ext.columns = ridx, cidx
|
| 745 |
+
styler = df_ext.style
|
| 746 |
+
|
| 747 |
+
func = lambda v: "bfseries: --rwrap" if "A" in v or "Z" in v or "c" in v else None
|
| 748 |
+
|
| 749 |
+
if index:
|
| 750 |
+
styler.map_index(func, axis="index")
|
| 751 |
+
if columns:
|
| 752 |
+
styler.map_index(func, axis="columns")
|
| 753 |
+
|
| 754 |
+
result = styler.to_latex(siunitx=siunitx)
|
| 755 |
+
|
| 756 |
+
expected_index = dedent(
|
| 757 |
+
"""\
|
| 758 |
+
\\multirow[c]{2}{*}{\\bfseries{A}} & a & 0 & -0.610000 & ab \\\\
|
| 759 |
+
\\bfseries{} & b & 1 & -1.220000 & cd \\\\
|
| 760 |
+
B & \\bfseries{c} & 2 & -2.220000 & de \\\\
|
| 761 |
+
"""
|
| 762 |
+
)
|
| 763 |
+
assert (expected_index in result) is index
|
| 764 |
+
|
| 765 |
+
exp_cols_si = dedent(
|
| 766 |
+
"""\
|
| 767 |
+
{} & {} & \\multicolumn{2}{r}{\\bfseries{Z}} & {Y} \\\\
|
| 768 |
+
{} & {} & {a} & {b} & {\\bfseries{c}} \\\\
|
| 769 |
+
"""
|
| 770 |
+
)
|
| 771 |
+
exp_cols_no_si = """\
|
| 772 |
+
& & \\multicolumn{2}{r}{\\bfseries{Z}} & Y \\\\
|
| 773 |
+
& & a & b & \\bfseries{c} \\\\
|
| 774 |
+
"""
|
| 775 |
+
assert ((exp_cols_si if siunitx else exp_cols_no_si) in result) is columns
|
| 776 |
+
|
| 777 |
+
|
| 778 |
+
def test_repr_option(styler):
|
| 779 |
+
assert "<style" in styler._repr_html_()[:6]
|
| 780 |
+
assert styler._repr_latex_() is None
|
| 781 |
+
with option_context("styler.render.repr", "latex"):
|
| 782 |
+
assert "\\begin{tabular}" in styler._repr_latex_()[:15]
|
| 783 |
+
assert styler._repr_html_() is None
|
| 784 |
+
|
| 785 |
+
|
| 786 |
+
@pytest.mark.parametrize("option", ["hrules"])
|
| 787 |
+
def test_bool_options(styler, option):
|
| 788 |
+
with option_context(f"styler.latex.{option}", False):
|
| 789 |
+
latex_false = styler.to_latex()
|
| 790 |
+
with option_context(f"styler.latex.{option}", True):
|
| 791 |
+
latex_true = styler.to_latex()
|
| 792 |
+
assert latex_false != latex_true # options are reactive under to_latex(*no_args)
|
| 793 |
+
|
| 794 |
+
|
| 795 |
+
def test_siunitx_basic_headers(styler):
|
| 796 |
+
assert "{} & {A} & {B} & {C} \\\\" in styler.to_latex(siunitx=True)
|
| 797 |
+
assert " & A & B & C \\\\" in styler.to_latex() # default siunitx=False
|
| 798 |
+
|
| 799 |
+
|
| 800 |
+
@pytest.mark.parametrize("axis", ["index", "columns"])
|
| 801 |
+
def test_css_convert_apply_index(styler, axis):
|
| 802 |
+
styler.map_index(lambda x: "font-weight: bold;", axis=axis)
|
| 803 |
+
for label in getattr(styler, axis):
|
| 804 |
+
assert f"\\bfseries {label}" in styler.to_latex(convert_css=True)
|
| 805 |
+
|
| 806 |
+
|
| 807 |
+
def test_hide_index_latex(styler):
|
| 808 |
+
# GH 43637
|
| 809 |
+
styler.hide([0], axis=0)
|
| 810 |
+
result = styler.to_latex()
|
| 811 |
+
expected = dedent(
|
| 812 |
+
"""\
|
| 813 |
+
\\begin{tabular}{lrrl}
|
| 814 |
+
& A & B & C \\\\
|
| 815 |
+
1 & 1 & -1.22 & cd \\\\
|
| 816 |
+
\\end{tabular}
|
| 817 |
+
"""
|
| 818 |
+
)
|
| 819 |
+
assert expected == result
|
| 820 |
+
|
| 821 |
+
|
| 822 |
+
def test_latex_hiding_index_columns_multiindex_alignment():
|
| 823 |
+
# gh 43644
|
| 824 |
+
midx = MultiIndex.from_product(
|
| 825 |
+
[["i0", "j0"], ["i1"], ["i2", "j2"]], names=["i-0", "i-1", "i-2"]
|
| 826 |
+
)
|
| 827 |
+
cidx = MultiIndex.from_product(
|
| 828 |
+
[["c0"], ["c1", "d1"], ["c2", "d2"]], names=["c-0", "c-1", "c-2"]
|
| 829 |
+
)
|
| 830 |
+
df = DataFrame(np.arange(16).reshape(4, 4), index=midx, columns=cidx)
|
| 831 |
+
styler = Styler(df, uuid_len=0)
|
| 832 |
+
styler.hide(level=1, axis=0).hide(level=0, axis=1)
|
| 833 |
+
styler.hide([("i0", "i1", "i2")], axis=0)
|
| 834 |
+
styler.hide([("c0", "c1", "c2")], axis=1)
|
| 835 |
+
styler.map(lambda x: "color:{red};" if x == 5 else "")
|
| 836 |
+
styler.map_index(lambda x: "color:{blue};" if "j" in x else "")
|
| 837 |
+
result = styler.to_latex()
|
| 838 |
+
expected = dedent(
|
| 839 |
+
"""\
|
| 840 |
+
\\begin{tabular}{llrrr}
|
| 841 |
+
& c-1 & c1 & \\multicolumn{2}{r}{d1} \\\\
|
| 842 |
+
& c-2 & d2 & c2 & d2 \\\\
|
| 843 |
+
i-0 & i-2 & & & \\\\
|
| 844 |
+
i0 & \\color{blue} j2 & \\color{red} 5 & 6 & 7 \\\\
|
| 845 |
+
\\multirow[c]{2}{*}{\\color{blue} j0} & i2 & 9 & 10 & 11 \\\\
|
| 846 |
+
\\color{blue} & \\color{blue} j2 & 13 & 14 & 15 \\\\
|
| 847 |
+
\\end{tabular}
|
| 848 |
+
"""
|
| 849 |
+
)
|
| 850 |
+
assert result == expected
|
| 851 |
+
|
| 852 |
+
|
| 853 |
+
def test_rendered_links():
|
| 854 |
+
# note the majority of testing is done in test_html.py: test_rendered_links
|
| 855 |
+
# these test only the alternative latex format is functional
|
| 856 |
+
df = DataFrame(["text www.domain.com text"])
|
| 857 |
+
result = df.style.format(hyperlinks="latex").to_latex()
|
| 858 |
+
assert r"text \href{www.domain.com}{www.domain.com} text" in result
|
| 859 |
+
|
| 860 |
+
|
| 861 |
+
def test_apply_index_hidden_levels():
|
| 862 |
+
# gh 45156
|
| 863 |
+
styler = DataFrame(
|
| 864 |
+
[[1]],
|
| 865 |
+
index=MultiIndex.from_tuples([(0, 1)], names=["l0", "l1"]),
|
| 866 |
+
columns=MultiIndex.from_tuples([(0, 1)], names=["c0", "c1"]),
|
| 867 |
+
).style
|
| 868 |
+
styler.hide(level=1)
|
| 869 |
+
styler.map_index(lambda v: "color: red;", level=0, axis=1)
|
| 870 |
+
result = styler.to_latex(convert_css=True)
|
| 871 |
+
expected = dedent(
|
| 872 |
+
"""\
|
| 873 |
+
\\begin{tabular}{lr}
|
| 874 |
+
c0 & \\color{red} 0 \\\\
|
| 875 |
+
c1 & 1 \\\\
|
| 876 |
+
l0 & \\\\
|
| 877 |
+
0 & 1 \\\\
|
| 878 |
+
\\end{tabular}
|
| 879 |
+
"""
|
| 880 |
+
)
|
| 881 |
+
assert result == expected
|
| 882 |
+
|
| 883 |
+
|
| 884 |
+
@pytest.mark.parametrize("clines", ["bad", "index", "skip-last", "all", "data"])
|
| 885 |
+
def test_clines_validation(clines, styler):
|
| 886 |
+
msg = f"`clines` value of {clines} is invalid."
|
| 887 |
+
with pytest.raises(ValueError, match=msg):
|
| 888 |
+
styler.to_latex(clines=clines)
|
| 889 |
+
|
| 890 |
+
|
| 891 |
+
@pytest.mark.parametrize(
|
| 892 |
+
"clines, exp",
|
| 893 |
+
[
|
| 894 |
+
("all;index", "\n\\cline{1-1}"),
|
| 895 |
+
("all;data", "\n\\cline{1-2}"),
|
| 896 |
+
("skip-last;index", ""),
|
| 897 |
+
("skip-last;data", ""),
|
| 898 |
+
(None, ""),
|
| 899 |
+
],
|
| 900 |
+
)
|
| 901 |
+
@pytest.mark.parametrize("env", ["table", "longtable"])
|
| 902 |
+
def test_clines_index(clines, exp, env):
|
| 903 |
+
df = DataFrame([[1], [2], [3], [4]])
|
| 904 |
+
result = df.style.to_latex(clines=clines, environment=env)
|
| 905 |
+
expected = f"""\
|
| 906 |
+
0 & 1 \\\\{exp}
|
| 907 |
+
1 & 2 \\\\{exp}
|
| 908 |
+
2 & 3 \\\\{exp}
|
| 909 |
+
3 & 4 \\\\{exp}
|
| 910 |
+
"""
|
| 911 |
+
assert expected in result
|
| 912 |
+
|
| 913 |
+
|
| 914 |
+
@pytest.mark.parametrize(
|
| 915 |
+
"clines, expected",
|
| 916 |
+
[
|
| 917 |
+
(
|
| 918 |
+
None,
|
| 919 |
+
dedent(
|
| 920 |
+
"""\
|
| 921 |
+
\\multirow[c]{2}{*}{A} & X & 1 \\\\
|
| 922 |
+
& Y & 2 \\\\
|
| 923 |
+
\\multirow[c]{2}{*}{B} & X & 3 \\\\
|
| 924 |
+
& Y & 4 \\\\
|
| 925 |
+
"""
|
| 926 |
+
),
|
| 927 |
+
),
|
| 928 |
+
(
|
| 929 |
+
"skip-last;index",
|
| 930 |
+
dedent(
|
| 931 |
+
"""\
|
| 932 |
+
\\multirow[c]{2}{*}{A} & X & 1 \\\\
|
| 933 |
+
& Y & 2 \\\\
|
| 934 |
+
\\cline{1-2}
|
| 935 |
+
\\multirow[c]{2}{*}{B} & X & 3 \\\\
|
| 936 |
+
& Y & 4 \\\\
|
| 937 |
+
\\cline{1-2}
|
| 938 |
+
"""
|
| 939 |
+
),
|
| 940 |
+
),
|
| 941 |
+
(
|
| 942 |
+
"skip-last;data",
|
| 943 |
+
dedent(
|
| 944 |
+
"""\
|
| 945 |
+
\\multirow[c]{2}{*}{A} & X & 1 \\\\
|
| 946 |
+
& Y & 2 \\\\
|
| 947 |
+
\\cline{1-3}
|
| 948 |
+
\\multirow[c]{2}{*}{B} & X & 3 \\\\
|
| 949 |
+
& Y & 4 \\\\
|
| 950 |
+
\\cline{1-3}
|
| 951 |
+
"""
|
| 952 |
+
),
|
| 953 |
+
),
|
| 954 |
+
(
|
| 955 |
+
"all;index",
|
| 956 |
+
dedent(
|
| 957 |
+
"""\
|
| 958 |
+
\\multirow[c]{2}{*}{A} & X & 1 \\\\
|
| 959 |
+
\\cline{2-2}
|
| 960 |
+
& Y & 2 \\\\
|
| 961 |
+
\\cline{1-2} \\cline{2-2}
|
| 962 |
+
\\multirow[c]{2}{*}{B} & X & 3 \\\\
|
| 963 |
+
\\cline{2-2}
|
| 964 |
+
& Y & 4 \\\\
|
| 965 |
+
\\cline{1-2} \\cline{2-2}
|
| 966 |
+
"""
|
| 967 |
+
),
|
| 968 |
+
),
|
| 969 |
+
(
|
| 970 |
+
"all;data",
|
| 971 |
+
dedent(
|
| 972 |
+
"""\
|
| 973 |
+
\\multirow[c]{2}{*}{A} & X & 1 \\\\
|
| 974 |
+
\\cline{2-3}
|
| 975 |
+
& Y & 2 \\\\
|
| 976 |
+
\\cline{1-3} \\cline{2-3}
|
| 977 |
+
\\multirow[c]{2}{*}{B} & X & 3 \\\\
|
| 978 |
+
\\cline{2-3}
|
| 979 |
+
& Y & 4 \\\\
|
| 980 |
+
\\cline{1-3} \\cline{2-3}
|
| 981 |
+
"""
|
| 982 |
+
),
|
| 983 |
+
),
|
| 984 |
+
],
|
| 985 |
+
)
|
| 986 |
+
@pytest.mark.parametrize("env", ["table"])
|
| 987 |
+
def test_clines_multiindex(clines, expected, env):
|
| 988 |
+
# also tests simultaneously with hidden rows and a hidden multiindex level
|
| 989 |
+
midx = MultiIndex.from_product([["A", "-", "B"], [0], ["X", "Y"]])
|
| 990 |
+
df = DataFrame([[1], [2], [99], [99], [3], [4]], index=midx)
|
| 991 |
+
styler = df.style
|
| 992 |
+
styler.hide([("-", 0, "X"), ("-", 0, "Y")])
|
| 993 |
+
styler.hide(level=1)
|
| 994 |
+
result = styler.to_latex(clines=clines, environment=env)
|
| 995 |
+
assert expected in result
|
| 996 |
+
|
| 997 |
+
|
| 998 |
+
def test_col_format_len(styler):
|
| 999 |
+
# gh 46037
|
| 1000 |
+
result = styler.to_latex(environment="longtable", column_format="lrr{10cm}")
|
| 1001 |
+
expected = r"\multicolumn{4}{r}{Continued on next page} \\"
|
| 1002 |
+
assert expected in result
|
| 1003 |
+
|
| 1004 |
+
|
| 1005 |
+
def test_concat(styler):
|
| 1006 |
+
result = styler.concat(styler.data.agg(["sum"]).style).to_latex()
|
| 1007 |
+
expected = dedent(
|
| 1008 |
+
"""\
|
| 1009 |
+
\\begin{tabular}{lrrl}
|
| 1010 |
+
& A & B & C \\\\
|
| 1011 |
+
0 & 0 & -0.61 & ab \\\\
|
| 1012 |
+
1 & 1 & -1.22 & cd \\\\
|
| 1013 |
+
sum & 1 & -1.830000 & abcd \\\\
|
| 1014 |
+
\\end{tabular}
|
| 1015 |
+
"""
|
| 1016 |
+
)
|
| 1017 |
+
assert result == expected
|
| 1018 |
+
|
| 1019 |
+
|
| 1020 |
+
def test_concat_recursion():
|
| 1021 |
+
# tests hidden row recursion and applied styles
|
| 1022 |
+
styler1 = DataFrame([[1], [9]]).style.hide([1]).highlight_min(color="red")
|
| 1023 |
+
styler2 = DataFrame([[9], [2]]).style.hide([0]).highlight_min(color="green")
|
| 1024 |
+
styler3 = DataFrame([[3], [9]]).style.hide([1]).highlight_min(color="blue")
|
| 1025 |
+
|
| 1026 |
+
result = styler1.concat(styler2.concat(styler3)).to_latex(convert_css=True)
|
| 1027 |
+
expected = dedent(
|
| 1028 |
+
"""\
|
| 1029 |
+
\\begin{tabular}{lr}
|
| 1030 |
+
& 0 \\\\
|
| 1031 |
+
0 & {\\cellcolor{red}} 1 \\\\
|
| 1032 |
+
1 & {\\cellcolor{green}} 2 \\\\
|
| 1033 |
+
0 & {\\cellcolor{blue}} 3 \\\\
|
| 1034 |
+
\\end{tabular}
|
| 1035 |
+
"""
|
| 1036 |
+
)
|
| 1037 |
+
assert result == expected
|
| 1038 |
+
|
| 1039 |
+
|
| 1040 |
+
def test_concat_chain():
|
| 1041 |
+
# tests hidden row recursion and applied styles
|
| 1042 |
+
styler1 = DataFrame([[1], [9]]).style.hide([1]).highlight_min(color="red")
|
| 1043 |
+
styler2 = DataFrame([[9], [2]]).style.hide([0]).highlight_min(color="green")
|
| 1044 |
+
styler3 = DataFrame([[3], [9]]).style.hide([1]).highlight_min(color="blue")
|
| 1045 |
+
|
| 1046 |
+
result = styler1.concat(styler2).concat(styler3).to_latex(convert_css=True)
|
| 1047 |
+
expected = dedent(
|
| 1048 |
+
"""\
|
| 1049 |
+
\\begin{tabular}{lr}
|
| 1050 |
+
& 0 \\\\
|
| 1051 |
+
0 & {\\cellcolor{red}} 1 \\\\
|
| 1052 |
+
1 & {\\cellcolor{green}} 2 \\\\
|
| 1053 |
+
0 & {\\cellcolor{blue}} 3 \\\\
|
| 1054 |
+
\\end{tabular}
|
| 1055 |
+
"""
|
| 1056 |
+
)
|
| 1057 |
+
assert result == expected
|
| 1058 |
+
|
| 1059 |
+
|
| 1060 |
+
@pytest.mark.parametrize(
|
| 1061 |
+
"columns, expected",
|
| 1062 |
+
[
|
| 1063 |
+
(
|
| 1064 |
+
None,
|
| 1065 |
+
dedent(
|
| 1066 |
+
"""\
|
| 1067 |
+
\\begin{tabular}{l}
|
| 1068 |
+
\\end{tabular}
|
| 1069 |
+
"""
|
| 1070 |
+
),
|
| 1071 |
+
),
|
| 1072 |
+
(
|
| 1073 |
+
["a", "b", "c"],
|
| 1074 |
+
dedent(
|
| 1075 |
+
"""\
|
| 1076 |
+
\\begin{tabular}{llll}
|
| 1077 |
+
& a & b & c \\\\
|
| 1078 |
+
\\end{tabular}
|
| 1079 |
+
"""
|
| 1080 |
+
),
|
| 1081 |
+
),
|
| 1082 |
+
],
|
| 1083 |
+
)
|
| 1084 |
+
@pytest.mark.parametrize(
|
| 1085 |
+
"clines", [None, "all;data", "all;index", "skip-last;data", "skip-last;index"]
|
| 1086 |
+
)
|
| 1087 |
+
def test_empty_clines(columns, expected: str, clines: str):
|
| 1088 |
+
# GH 47203
|
| 1089 |
+
df = DataFrame(columns=columns)
|
| 1090 |
+
result = df.style.to_latex(clines=clines)
|
| 1091 |
+
assert result == expected
|
.venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_to_string.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from textwrap import dedent
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
from pandas import (
|
| 6 |
+
DataFrame,
|
| 7 |
+
Series,
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
pytest.importorskip("jinja2")
|
| 11 |
+
from pandas.io.formats.style import Styler
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@pytest.fixture
|
| 15 |
+
def df():
|
| 16 |
+
return DataFrame(
|
| 17 |
+
{"A": [0, 1], "B": [-0.61, -1.22], "C": Series(["ab", "cd"], dtype=object)}
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@pytest.fixture
|
| 22 |
+
def styler(df):
|
| 23 |
+
return Styler(df, uuid_len=0, precision=2)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def test_basic_string(styler):
|
| 27 |
+
result = styler.to_string()
|
| 28 |
+
expected = dedent(
|
| 29 |
+
"""\
|
| 30 |
+
A B C
|
| 31 |
+
0 0 -0.61 ab
|
| 32 |
+
1 1 -1.22 cd
|
| 33 |
+
"""
|
| 34 |
+
)
|
| 35 |
+
assert result == expected
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def test_string_delimiter(styler):
|
| 39 |
+
result = styler.to_string(delimiter=";")
|
| 40 |
+
expected = dedent(
|
| 41 |
+
"""\
|
| 42 |
+
;A;B;C
|
| 43 |
+
0;0;-0.61;ab
|
| 44 |
+
1;1;-1.22;cd
|
| 45 |
+
"""
|
| 46 |
+
)
|
| 47 |
+
assert result == expected
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def test_concat(styler):
|
| 51 |
+
result = styler.concat(styler.data.agg(["sum"]).style).to_string()
|
| 52 |
+
expected = dedent(
|
| 53 |
+
"""\
|
| 54 |
+
A B C
|
| 55 |
+
0 0 -0.61 ab
|
| 56 |
+
1 1 -1.22 cd
|
| 57 |
+
sum 1 -1.830000 abcd
|
| 58 |
+
"""
|
| 59 |
+
)
|
| 60 |
+
assert result == expected
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def test_concat_recursion(styler):
|
| 64 |
+
df = styler.data
|
| 65 |
+
styler1 = styler
|
| 66 |
+
styler2 = Styler(df.agg(["sum"]), uuid_len=0, precision=3)
|
| 67 |
+
styler3 = Styler(df.agg(["sum"]), uuid_len=0, precision=4)
|
| 68 |
+
result = styler1.concat(styler2.concat(styler3)).to_string()
|
| 69 |
+
expected = dedent(
|
| 70 |
+
"""\
|
| 71 |
+
A B C
|
| 72 |
+
0 0 -0.61 ab
|
| 73 |
+
1 1 -1.22 cd
|
| 74 |
+
sum 1 -1.830 abcd
|
| 75 |
+
sum 1 -1.8300 abcd
|
| 76 |
+
"""
|
| 77 |
+
)
|
| 78 |
+
assert result == expected
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def test_concat_chain(styler):
|
| 82 |
+
df = styler.data
|
| 83 |
+
styler1 = styler
|
| 84 |
+
styler2 = Styler(df.agg(["sum"]), uuid_len=0, precision=3)
|
| 85 |
+
styler3 = Styler(df.agg(["sum"]), uuid_len=0, precision=4)
|
| 86 |
+
result = styler1.concat(styler2).concat(styler3).to_string()
|
| 87 |
+
expected = dedent(
|
| 88 |
+
"""\
|
| 89 |
+
A B C
|
| 90 |
+
0 0 -0.61 ab
|
| 91 |
+
1 1 -1.22 cd
|
| 92 |
+
sum 1 -1.830 abcd
|
| 93 |
+
sum 1 -1.8300 abcd
|
| 94 |
+
"""
|
| 95 |
+
)
|
| 96 |
+
assert result == expected
|
.venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_to_typst.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from textwrap import dedent
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
from pandas import (
|
| 6 |
+
DataFrame,
|
| 7 |
+
Series,
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
pytest.importorskip("jinja2")
|
| 11 |
+
from pandas.io.formats.style import Styler
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@pytest.fixture
|
| 15 |
+
def df():
|
| 16 |
+
return DataFrame(
|
| 17 |
+
{"A": [0, 1], "B": [-0.61, -1.22], "C": Series(["ab", "cd"], dtype=object)}
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@pytest.fixture
|
| 22 |
+
def styler(df):
|
| 23 |
+
return Styler(df, uuid_len=0, precision=2)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def test_basic_table(styler):
|
| 27 |
+
result = styler.to_typst()
|
| 28 |
+
expected = dedent(
|
| 29 |
+
"""\
|
| 30 |
+
#table(
|
| 31 |
+
columns: 4,
|
| 32 |
+
[], [A], [B], [C],
|
| 33 |
+
|
| 34 |
+
[0], [0], [-0.61], [ab],
|
| 35 |
+
[1], [1], [-1.22], [cd],
|
| 36 |
+
)"""
|
| 37 |
+
)
|
| 38 |
+
assert result == expected
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def test_concat(styler):
|
| 42 |
+
result = styler.concat(styler.data.agg(["sum"]).style).to_typst()
|
| 43 |
+
expected = dedent(
|
| 44 |
+
"""\
|
| 45 |
+
#table(
|
| 46 |
+
columns: 4,
|
| 47 |
+
[], [A], [B], [C],
|
| 48 |
+
|
| 49 |
+
[0], [0], [-0.61], [ab],
|
| 50 |
+
[1], [1], [-1.22], [cd],
|
| 51 |
+
[sum], [1], [-1.830000], [abcd],
|
| 52 |
+
)"""
|
| 53 |
+
)
|
| 54 |
+
assert result == expected
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def test_concat_recursion(styler):
|
| 58 |
+
df = styler.data
|
| 59 |
+
styler1 = styler
|
| 60 |
+
styler2 = Styler(df.agg(["sum"]), uuid_len=0, precision=3)
|
| 61 |
+
styler3 = Styler(df.agg(["sum"]), uuid_len=0, precision=4)
|
| 62 |
+
result = styler1.concat(styler2.concat(styler3)).to_typst()
|
| 63 |
+
expected = dedent(
|
| 64 |
+
"""\
|
| 65 |
+
#table(
|
| 66 |
+
columns: 4,
|
| 67 |
+
[], [A], [B], [C],
|
| 68 |
+
|
| 69 |
+
[0], [0], [-0.61], [ab],
|
| 70 |
+
[1], [1], [-1.22], [cd],
|
| 71 |
+
[sum], [1], [-1.830], [abcd],
|
| 72 |
+
[sum], [1], [-1.8300], [abcd],
|
| 73 |
+
)"""
|
| 74 |
+
)
|
| 75 |
+
assert result == expected
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def test_concat_chain(styler):
|
| 79 |
+
df = styler.data
|
| 80 |
+
styler1 = styler
|
| 81 |
+
styler2 = Styler(df.agg(["sum"]), uuid_len=0, precision=3)
|
| 82 |
+
styler3 = Styler(df.agg(["sum"]), uuid_len=0, precision=4)
|
| 83 |
+
result = styler1.concat(styler2).concat(styler3).to_typst()
|
| 84 |
+
expected = dedent(
|
| 85 |
+
"""\
|
| 86 |
+
#table(
|
| 87 |
+
columns: 4,
|
| 88 |
+
[], [A], [B], [C],
|
| 89 |
+
|
| 90 |
+
[0], [0], [-0.61], [ab],
|
| 91 |
+
[1], [1], [-1.22], [cd],
|
| 92 |
+
[sum], [1], [-1.830], [abcd],
|
| 93 |
+
[sum], [1], [-1.8300], [abcd],
|
| 94 |
+
)"""
|
| 95 |
+
)
|
| 96 |
+
assert result == expected
|
.venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_tooltip.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pytest
|
| 3 |
+
|
| 4 |
+
from pandas import (
|
| 5 |
+
DataFrame,
|
| 6 |
+
MultiIndex,
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
pytest.importorskip("jinja2")
|
| 10 |
+
from pandas.io.formats.style import Styler
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@pytest.fixture
|
| 14 |
+
def df():
|
| 15 |
+
return DataFrame(
|
| 16 |
+
data=[[0, 1, 2], [3, 4, 5], [6, 7, 8]],
|
| 17 |
+
columns=["A", "B", "C"],
|
| 18 |
+
index=["x", "y", "z"],
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@pytest.fixture
|
| 23 |
+
def styler(df):
|
| 24 |
+
return Styler(df, uuid_len=0)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@pytest.mark.parametrize(
|
| 28 |
+
"data, columns, index",
|
| 29 |
+
[
|
| 30 |
+
# Test basic reindex and ignoring blank
|
| 31 |
+
([["Min", "Max"], [np.nan, ""]], ["A", "C"], ["x", "y"]),
|
| 32 |
+
# Test non-referenced columns, reversed col names, short index
|
| 33 |
+
([["Max", "Min", "Bad-Col"]], ["C", "A", "D"], ["x"]),
|
| 34 |
+
],
|
| 35 |
+
)
|
| 36 |
+
def test_tooltip_render(data, columns, index, styler):
|
| 37 |
+
ttips = DataFrame(data=data, columns=columns, index=index)
|
| 38 |
+
|
| 39 |
+
# GH 21266
|
| 40 |
+
result = styler.set_tooltips(ttips).to_html()
|
| 41 |
+
|
| 42 |
+
# test tooltip table level class
|
| 43 |
+
assert "#T_ .pd-t {\n visibility: hidden;\n" in result
|
| 44 |
+
|
| 45 |
+
# test 'Min' tooltip added
|
| 46 |
+
assert "#T_ #T__row0_col0:hover .pd-t {\n visibility: visible;\n}" in result
|
| 47 |
+
assert '#T_ #T__row0_col0 .pd-t::after {\n content: "Min";\n}' in result
|
| 48 |
+
assert 'class="data row0 col0" >0<span class="pd-t"></span></td>' in result
|
| 49 |
+
|
| 50 |
+
# test 'Max' tooltip added
|
| 51 |
+
assert "#T_ #T__row0_col2:hover .pd-t {\n visibility: visible;\n}" in result
|
| 52 |
+
assert '#T_ #T__row0_col2 .pd-t::after {\n content: "Max";\n}' in result
|
| 53 |
+
assert 'class="data row0 col2" >2<span class="pd-t"></span></td>' in result
|
| 54 |
+
|
| 55 |
+
# test Nan, empty string and bad column ignored
|
| 56 |
+
assert "#T_ #T__row1_col0:hover .pd-t {\n visibility: visible;\n}" not in result
|
| 57 |
+
assert "#T_ #T__row1_col1:hover .pd-t {\n visibility: visible;\n}" not in result
|
| 58 |
+
assert "#T_ #T__row0_col1:hover .pd-t {\n visibility: visible;\n}" not in result
|
| 59 |
+
assert "#T_ #T__row1_col2:hover .pd-t {\n visibility: visible;\n}" not in result
|
| 60 |
+
assert "Bad-Col" not in result
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def test_tooltip_ignored(styler):
|
| 64 |
+
# GH 21266
|
| 65 |
+
result = styler.to_html() # no set_tooltips() creates no <span>
|
| 66 |
+
assert '<style type="text/css">\n</style>' in result
|
| 67 |
+
assert '<span class="pd-t"></span>' not in result
|
| 68 |
+
assert 'title="' not in result
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def test_tooltip_css_class(styler):
|
| 72 |
+
# GH 21266
|
| 73 |
+
result = styler.set_tooltips(
|
| 74 |
+
DataFrame([["tooltip"]], index=["x"], columns=["A"]),
|
| 75 |
+
css_class="other-class",
|
| 76 |
+
props=[("color", "green")],
|
| 77 |
+
).to_html()
|
| 78 |
+
assert "#T_ .other-class {\n color: green;\n" in result
|
| 79 |
+
assert '#T_ #T__row0_col0 .other-class::after {\n content: "tooltip";\n' in result
|
| 80 |
+
|
| 81 |
+
# GH 39563
|
| 82 |
+
result = styler.set_tooltips( # set_tooltips overwrites previous
|
| 83 |
+
DataFrame([["tooltip"]], index=["x"], columns=["A"]),
|
| 84 |
+
css_class="another-class",
|
| 85 |
+
props="color:green;color:red;",
|
| 86 |
+
).to_html()
|
| 87 |
+
assert "#T_ .another-class {\n color: green;\n color: red;\n}" in result
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
@pytest.mark.parametrize(
|
| 91 |
+
"data, columns, index",
|
| 92 |
+
[
|
| 93 |
+
# Test basic reindex and ignoring blank
|
| 94 |
+
([["Min", "Max"], [np.nan, ""]], ["A", "C"], ["x", "y"]),
|
| 95 |
+
# Test non-referenced columns, reversed col names, short index
|
| 96 |
+
([["Max", "Min", "Bad-Col"]], ["C", "A", "D"], ["x"]),
|
| 97 |
+
],
|
| 98 |
+
)
|
| 99 |
+
def test_tooltip_render_as_title(data, columns, index, styler):
|
| 100 |
+
ttips = DataFrame(data=data, columns=columns, index=index)
|
| 101 |
+
# GH 56605
|
| 102 |
+
result = styler.set_tooltips(ttips, as_title_attribute=True).to_html()
|
| 103 |
+
|
| 104 |
+
# test css not added
|
| 105 |
+
assert "#T_ .pd-t {\n visibility: hidden;\n" not in result
|
| 106 |
+
|
| 107 |
+
# test 'Min' tooltip added as title attribute and css does not exist
|
| 108 |
+
assert "#T_ #T__row0_col0:hover .pd-t {\n visibility: visible;\n}" not in result
|
| 109 |
+
assert '#T_ #T__row0_col0 .pd-t::after {\n content: "Min";\n}' not in result
|
| 110 |
+
assert 'class="data row0 col0" title="Min">0</td>' in result
|
| 111 |
+
|
| 112 |
+
# test 'Max' tooltip added as title attribute and css does not exist
|
| 113 |
+
assert "#T_ #T__row0_col2:hover .pd-t {\n visibility: visible;\n}" not in result
|
| 114 |
+
assert '#T_ #T__row0_col2 .pd-t::after {\n content: "Max";\n}' not in result
|
| 115 |
+
assert 'class="data row0 col2" title="Max">2</td>' in result
|
| 116 |
+
|
| 117 |
+
# test Nan, empty string and bad column ignored
|
| 118 |
+
assert "#T_ #T__row1_col0:hover .pd-t {\n visibility: visible;\n}" not in result
|
| 119 |
+
assert "#T_ #T__row1_col1:hover .pd-t {\n visibility: visible;\n}" not in result
|
| 120 |
+
assert "#T_ #T__row0_col1:hover .pd-t {\n visibility: visible;\n}" not in result
|
| 121 |
+
assert "#T_ #T__row1_col2:hover .pd-t {\n visibility: visible;\n}" not in result
|
| 122 |
+
assert "Bad-Col" not in result
|
| 123 |
+
assert 'class="data row0 col1" >1</td>' in result
|
| 124 |
+
assert 'class="data row1 col0" >3</td>' in result
|
| 125 |
+
assert 'class="data row1 col1" >4</td>' in result
|
| 126 |
+
assert 'class="data row1 col2" >5</td>' in result
|
| 127 |
+
assert 'class="data row2 col0" >6</td>' in result
|
| 128 |
+
assert 'class="data row2 col1" >7</td>' in result
|
| 129 |
+
assert 'class="data row2 col2" >8</td>' in result
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def test_tooltip_render_as_title_with_hidden_index_level():
|
| 133 |
+
df = DataFrame(
|
| 134 |
+
data=[[0, 1, 2], [3, 4, 5], [6, 7, 8]],
|
| 135 |
+
columns=["A", "B", "C"],
|
| 136 |
+
index=MultiIndex.from_arrays(
|
| 137 |
+
[["x", "y", "z"], [1, 2, 3], ["aa", "bb", "cc"]],
|
| 138 |
+
names=["alpha", "num", "char"],
|
| 139 |
+
),
|
| 140 |
+
)
|
| 141 |
+
ttips = DataFrame(
|
| 142 |
+
# Test basic reindex and ignoring blank, and hide level 2 (num) from index
|
| 143 |
+
data=[["Min", "Max"], [np.nan, ""]],
|
| 144 |
+
columns=["A", "C"],
|
| 145 |
+
index=MultiIndex.from_arrays(
|
| 146 |
+
[["x", "y"], [1, 2], ["aa", "bb"]], names=["alpha", "num", "char"]
|
| 147 |
+
),
|
| 148 |
+
)
|
| 149 |
+
styler = Styler(df, uuid_len=0)
|
| 150 |
+
styler = styler.hide(axis=0, level=-1, names=True)
|
| 151 |
+
# GH 56605
|
| 152 |
+
result = styler.set_tooltips(ttips, as_title_attribute=True).to_html()
|
| 153 |
+
|
| 154 |
+
# test css not added
|
| 155 |
+
assert "#T_ .pd-t {\n visibility: hidden;\n" not in result
|
| 156 |
+
|
| 157 |
+
# test 'Min' tooltip added as title attribute and css does not exist
|
| 158 |
+
assert "#T_ #T__row0_col0:hover .pd-t {\n visibility: visible;\n}" not in result
|
| 159 |
+
assert '#T_ #T__row0_col0 .pd-t::after {\n content: "Min";\n}' not in result
|
| 160 |
+
assert 'class="data row0 col0" title="Min">0</td>' in result
|
| 161 |
+
|
| 162 |
+
# test 'Max' tooltip added as title attribute and css does not exist
|
| 163 |
+
assert "#T_ #T__row0_col2:hover .pd-t {\n visibility: visible;\n}" not in result
|
| 164 |
+
assert '#T_ #T__row0_col2 .pd-t::after {\n content: "Max";\n}' not in result
|
| 165 |
+
assert 'class="data row0 col2" title="Max">2</td>' in result
|
| 166 |
+
|
| 167 |
+
# test Nan, empty string and bad column ignored
|
| 168 |
+
assert "#T_ #T__row1_col0:hover .pd-t {\n visibility: visible;\n}" not in result
|
| 169 |
+
assert "#T_ #T__row1_col1:hover .pd-t {\n visibility: visible;\n}" not in result
|
| 170 |
+
assert "#T_ #T__row0_col1:hover .pd-t {\n visibility: visible;\n}" not in result
|
| 171 |
+
assert "#T_ #T__row1_col2:hover .pd-t {\n visibility: visible;\n}" not in result
|
| 172 |
+
assert "Bad-Col" not in result
|
| 173 |
+
assert 'class="data row0 col1" >1</td>' in result
|
| 174 |
+
assert 'class="data row1 col0" >3</td>' in result
|
| 175 |
+
assert 'class="data row1 col1" >4</td>' in result
|
| 176 |
+
assert 'class="data row1 col2" >5</td>' in result
|
| 177 |
+
assert 'class="data row2 col0" >6</td>' in result
|
| 178 |
+
assert 'class="data row2 col1" >7</td>' in result
|
| 179 |
+
assert 'class="data row2 col2" >8</td>' in result
|
.venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_console.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import locale
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
from pandas._config import detect_console_encoding
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class MockEncoding:
|
| 9 |
+
"""
|
| 10 |
+
Used to add a side effect when accessing the 'encoding' property. If the
|
| 11 |
+
side effect is a str in nature, the value will be returned. Otherwise, the
|
| 12 |
+
side effect should be an exception that will be raised.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
def __init__(self, encoding) -> None:
|
| 16 |
+
super().__init__()
|
| 17 |
+
self.val = encoding
|
| 18 |
+
|
| 19 |
+
@property
|
| 20 |
+
def encoding(self):
|
| 21 |
+
return self.raise_or_return(self.val)
|
| 22 |
+
|
| 23 |
+
@staticmethod
|
| 24 |
+
def raise_or_return(val):
|
| 25 |
+
if isinstance(val, str):
|
| 26 |
+
return val
|
| 27 |
+
else:
|
| 28 |
+
raise val
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@pytest.mark.parametrize("empty,filled", [["stdin", "stdout"], ["stdout", "stdin"]])
|
| 32 |
+
def test_detect_console_encoding_from_stdout_stdin(monkeypatch, empty, filled):
|
| 33 |
+
# Ensures that when sys.stdout.encoding or sys.stdin.encoding is used when
|
| 34 |
+
# they have values filled.
|
| 35 |
+
# GH 21552
|
| 36 |
+
with monkeypatch.context() as context:
|
| 37 |
+
context.setattr(f"sys.{empty}", MockEncoding(""))
|
| 38 |
+
context.setattr(f"sys.{filled}", MockEncoding(filled))
|
| 39 |
+
assert detect_console_encoding() == filled
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@pytest.mark.parametrize("encoding", [AttributeError, OSError, "ascii"])
|
| 43 |
+
def test_detect_console_encoding_fallback_to_locale(monkeypatch, encoding):
|
| 44 |
+
# GH 21552
|
| 45 |
+
with monkeypatch.context() as context:
|
| 46 |
+
context.setattr("locale.getpreferredencoding", lambda: "foo")
|
| 47 |
+
context.setattr("sys.stdout", MockEncoding(encoding))
|
| 48 |
+
assert detect_console_encoding() == "foo"
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@pytest.mark.parametrize(
|
| 52 |
+
"std,locale",
|
| 53 |
+
[
|
| 54 |
+
["ascii", "ascii"],
|
| 55 |
+
["ascii", locale.Error],
|
| 56 |
+
[AttributeError, "ascii"],
|
| 57 |
+
[AttributeError, locale.Error],
|
| 58 |
+
[OSError, "ascii"],
|
| 59 |
+
[OSError, locale.Error],
|
| 60 |
+
],
|
| 61 |
+
)
|
| 62 |
+
def test_detect_console_encoding_fallback_to_default(monkeypatch, std, locale):
|
| 63 |
+
# When both the stdout/stdin encoding and locale preferred encoding checks
|
| 64 |
+
# fail (or return 'ascii', we should default to the sys default encoding.
|
| 65 |
+
# GH 21552
|
| 66 |
+
with monkeypatch.context() as context:
|
| 67 |
+
context.setattr(
|
| 68 |
+
"locale.getpreferredencoding", lambda: MockEncoding.raise_or_return(locale)
|
| 69 |
+
)
|
| 70 |
+
context.setattr("sys.stdout", MockEncoding(std))
|
| 71 |
+
context.setattr("sys.getdefaultencoding", lambda: "sysDefaultEncoding")
|
| 72 |
+
assert detect_console_encoding() == "sysDefaultEncoding"
|
.venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_css.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
+
from pandas.errors import CSSWarning
|
| 4 |
+
|
| 5 |
+
import pandas._testing as tm
|
| 6 |
+
|
| 7 |
+
from pandas.io.formats.css import CSSResolver
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def assert_resolves(css, props, inherited=None):
|
| 11 |
+
resolve = CSSResolver()
|
| 12 |
+
actual = resolve(css, inherited=inherited)
|
| 13 |
+
assert props == actual
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def assert_same_resolution(css1, css2, inherited=None):
|
| 17 |
+
resolve = CSSResolver()
|
| 18 |
+
resolved1 = resolve(css1, inherited=inherited)
|
| 19 |
+
resolved2 = resolve(css2, inherited=inherited)
|
| 20 |
+
assert resolved1 == resolved2
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@pytest.mark.parametrize(
|
| 24 |
+
"name,norm,abnorm",
|
| 25 |
+
[
|
| 26 |
+
(
|
| 27 |
+
"whitespace",
|
| 28 |
+
"hello: world; foo: bar",
|
| 29 |
+
" \t hello \t :\n world \n ; \n foo: \tbar\n\n",
|
| 30 |
+
),
|
| 31 |
+
("case", "hello: world; foo: bar", "Hello: WORLD; foO: bar"),
|
| 32 |
+
("empty-decl", "hello: world; foo: bar", "; hello: world;; foo: bar;\n; ;"),
|
| 33 |
+
("empty-list", "", ";"),
|
| 34 |
+
],
|
| 35 |
+
)
|
| 36 |
+
def test_css_parse_normalisation(name, norm, abnorm):
|
| 37 |
+
assert_same_resolution(norm, abnorm)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@pytest.mark.parametrize(
|
| 41 |
+
"invalid_css,remainder,msg",
|
| 42 |
+
[
|
| 43 |
+
# No colon
|
| 44 |
+
("hello-world", "", "expected a colon"),
|
| 45 |
+
("border-style: solid; hello-world", "border-style: solid", "expected a colon"),
|
| 46 |
+
(
|
| 47 |
+
"border-style: solid; hello-world; font-weight: bold",
|
| 48 |
+
"border-style: solid; font-weight: bold",
|
| 49 |
+
"expected a colon",
|
| 50 |
+
),
|
| 51 |
+
# Unclosed string fail
|
| 52 |
+
# Invalid size
|
| 53 |
+
("font-size: blah", "font-size: 1em", "Unhandled size"),
|
| 54 |
+
("font-size: 1a2b", "font-size: 1em", "Unhandled size"),
|
| 55 |
+
("font-size: 1e5pt", "font-size: 1em", "Unhandled size"),
|
| 56 |
+
("font-size: 1+6pt", "font-size: 1em", "Unhandled size"),
|
| 57 |
+
("font-size: 1unknownunit", "font-size: 1em", "Unhandled size"),
|
| 58 |
+
("font-size: 10", "font-size: 1em", "Unhandled size"),
|
| 59 |
+
("font-size: 10 pt", "font-size: 1em", "Unhandled size"),
|
| 60 |
+
# Too many args
|
| 61 |
+
("border-top: 1pt solid red green", "border-top: 1pt solid green", "Too many"),
|
| 62 |
+
],
|
| 63 |
+
)
|
| 64 |
+
def test_css_parse_invalid(invalid_css, remainder, msg):
|
| 65 |
+
with tm.assert_produces_warning(CSSWarning, match=msg):
|
| 66 |
+
assert_same_resolution(invalid_css, remainder)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
@pytest.mark.parametrize(
|
| 70 |
+
"shorthand,expansions",
|
| 71 |
+
[
|
| 72 |
+
("margin", ["margin-top", "margin-right", "margin-bottom", "margin-left"]),
|
| 73 |
+
("padding", ["padding-top", "padding-right", "padding-bottom", "padding-left"]),
|
| 74 |
+
(
|
| 75 |
+
"border-width",
|
| 76 |
+
[
|
| 77 |
+
"border-top-width",
|
| 78 |
+
"border-right-width",
|
| 79 |
+
"border-bottom-width",
|
| 80 |
+
"border-left-width",
|
| 81 |
+
],
|
| 82 |
+
),
|
| 83 |
+
(
|
| 84 |
+
"border-color",
|
| 85 |
+
[
|
| 86 |
+
"border-top-color",
|
| 87 |
+
"border-right-color",
|
| 88 |
+
"border-bottom-color",
|
| 89 |
+
"border-left-color",
|
| 90 |
+
],
|
| 91 |
+
),
|
| 92 |
+
(
|
| 93 |
+
"border-style",
|
| 94 |
+
[
|
| 95 |
+
"border-top-style",
|
| 96 |
+
"border-right-style",
|
| 97 |
+
"border-bottom-style",
|
| 98 |
+
"border-left-style",
|
| 99 |
+
],
|
| 100 |
+
),
|
| 101 |
+
],
|
| 102 |
+
)
|
| 103 |
+
def test_css_side_shorthands(shorthand, expansions):
|
| 104 |
+
top, right, bottom, left = expansions
|
| 105 |
+
|
| 106 |
+
assert_resolves(
|
| 107 |
+
f"{shorthand}: 1pt", {top: "1pt", right: "1pt", bottom: "1pt", left: "1pt"}
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
assert_resolves(
|
| 111 |
+
f"{shorthand}: 1pt 4pt", {top: "1pt", right: "4pt", bottom: "1pt", left: "4pt"}
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
assert_resolves(
|
| 115 |
+
f"{shorthand}: 1pt 4pt 2pt",
|
| 116 |
+
{top: "1pt", right: "4pt", bottom: "2pt", left: "4pt"},
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
assert_resolves(
|
| 120 |
+
f"{shorthand}: 1pt 4pt 2pt 0pt",
|
| 121 |
+
{top: "1pt", right: "4pt", bottom: "2pt", left: "0pt"},
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
with tm.assert_produces_warning(CSSWarning, match="Could not expand"):
|
| 125 |
+
assert_resolves(f"{shorthand}: 1pt 1pt 1pt 1pt 1pt", {})
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
@pytest.mark.parametrize(
|
| 129 |
+
"shorthand,sides",
|
| 130 |
+
[
|
| 131 |
+
("border-top", ["top"]),
|
| 132 |
+
("border-right", ["right"]),
|
| 133 |
+
("border-bottom", ["bottom"]),
|
| 134 |
+
("border-left", ["left"]),
|
| 135 |
+
("border", ["top", "right", "bottom", "left"]),
|
| 136 |
+
],
|
| 137 |
+
)
|
| 138 |
+
def test_css_border_shorthand_sides(shorthand, sides):
|
| 139 |
+
def create_border_dict(sides, color=None, style=None, width=None):
|
| 140 |
+
resolved = {}
|
| 141 |
+
for side in sides:
|
| 142 |
+
if color:
|
| 143 |
+
resolved[f"border-{side}-color"] = color
|
| 144 |
+
if style:
|
| 145 |
+
resolved[f"border-{side}-style"] = style
|
| 146 |
+
if width:
|
| 147 |
+
resolved[f"border-{side}-width"] = width
|
| 148 |
+
return resolved
|
| 149 |
+
|
| 150 |
+
assert_resolves(
|
| 151 |
+
f"{shorthand}: 1pt red solid", create_border_dict(sides, "red", "solid", "1pt")
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
@pytest.mark.parametrize(
|
| 156 |
+
"prop, expected",
|
| 157 |
+
[
|
| 158 |
+
("1pt red solid", ("red", "solid", "1pt")),
|
| 159 |
+
("red 1pt solid", ("red", "solid", "1pt")),
|
| 160 |
+
("red solid 1pt", ("red", "solid", "1pt")),
|
| 161 |
+
("solid 1pt red", ("red", "solid", "1pt")),
|
| 162 |
+
("red solid", ("red", "solid", "1.500000pt")),
|
| 163 |
+
# Note: color=black is not CSS conforming
|
| 164 |
+
# (See https://drafts.csswg.org/css-backgrounds/#border-shorthands)
|
| 165 |
+
("1pt solid", ("black", "solid", "1pt")),
|
| 166 |
+
("1pt red", ("red", "none", "1pt")),
|
| 167 |
+
("red", ("red", "none", "1.500000pt")),
|
| 168 |
+
("1pt", ("black", "none", "1pt")),
|
| 169 |
+
("solid", ("black", "solid", "1.500000pt")),
|
| 170 |
+
# Sizes
|
| 171 |
+
("1em", ("black", "none", "12pt")),
|
| 172 |
+
],
|
| 173 |
+
)
|
| 174 |
+
def test_css_border_shorthands(prop, expected):
|
| 175 |
+
color, style, width = expected
|
| 176 |
+
|
| 177 |
+
assert_resolves(
|
| 178 |
+
f"border-left: {prop}",
|
| 179 |
+
{
|
| 180 |
+
"border-left-color": color,
|
| 181 |
+
"border-left-style": style,
|
| 182 |
+
"border-left-width": width,
|
| 183 |
+
},
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
@pytest.mark.parametrize(
|
| 188 |
+
"style,inherited,equiv",
|
| 189 |
+
[
|
| 190 |
+
("margin: 1px; margin: 2px", "", "margin: 2px"),
|
| 191 |
+
("margin: 1px", "margin: 2px", "margin: 1px"),
|
| 192 |
+
("margin: 1px; margin: inherit", "margin: 2px", "margin: 2px"),
|
| 193 |
+
(
|
| 194 |
+
"margin: 1px; margin-top: 2px",
|
| 195 |
+
"",
|
| 196 |
+
"margin-left: 1px; margin-right: 1px; margin-bottom: 1px; margin-top: 2px",
|
| 197 |
+
),
|
| 198 |
+
("margin-top: 2px", "margin: 1px", "margin: 1px; margin-top: 2px"),
|
| 199 |
+
("margin: 1px", "margin-top: 2px", "margin: 1px"),
|
| 200 |
+
(
|
| 201 |
+
"margin: 1px; margin-top: inherit",
|
| 202 |
+
"margin: 2px",
|
| 203 |
+
"margin: 1px; margin-top: 2px",
|
| 204 |
+
),
|
| 205 |
+
],
|
| 206 |
+
)
|
| 207 |
+
def test_css_precedence(style, inherited, equiv):
|
| 208 |
+
resolve = CSSResolver()
|
| 209 |
+
inherited_props = resolve(inherited)
|
| 210 |
+
style_props = resolve(style, inherited=inherited_props)
|
| 211 |
+
equiv_props = resolve(equiv)
|
| 212 |
+
assert style_props == equiv_props
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
@pytest.mark.parametrize(
|
| 216 |
+
"style,equiv",
|
| 217 |
+
[
|
| 218 |
+
(
|
| 219 |
+
"margin: 1px; margin-top: inherit",
|
| 220 |
+
"margin-bottom: 1px; margin-right: 1px; margin-left: 1px",
|
| 221 |
+
),
|
| 222 |
+
("margin-top: inherit", ""),
|
| 223 |
+
("margin-top: initial", ""),
|
| 224 |
+
],
|
| 225 |
+
)
|
| 226 |
+
def test_css_none_absent(style, equiv):
|
| 227 |
+
assert_same_resolution(style, equiv)
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
@pytest.mark.parametrize(
|
| 231 |
+
"size,resolved",
|
| 232 |
+
[
|
| 233 |
+
("xx-small", "6pt"),
|
| 234 |
+
("x-small", f"{7.5:f}pt"),
|
| 235 |
+
("small", f"{9.6:f}pt"),
|
| 236 |
+
("medium", "12pt"),
|
| 237 |
+
("large", f"{13.5:f}pt"),
|
| 238 |
+
("x-large", "18pt"),
|
| 239 |
+
("xx-large", "24pt"),
|
| 240 |
+
("8px", "6pt"),
|
| 241 |
+
("1.25pc", "15pt"),
|
| 242 |
+
(".25in", "18pt"),
|
| 243 |
+
("02.54cm", "72pt"),
|
| 244 |
+
("25.4mm", "72pt"),
|
| 245 |
+
("101.6q", "72pt"),
|
| 246 |
+
],
|
| 247 |
+
)
|
| 248 |
+
@pytest.mark.parametrize("relative_to", [None, "16pt"]) # invariant to inherited size
|
| 249 |
+
def test_css_absolute_font_size(size, relative_to, resolved):
|
| 250 |
+
if relative_to is None:
|
| 251 |
+
inherited = None
|
| 252 |
+
else:
|
| 253 |
+
inherited = {"font-size": relative_to}
|
| 254 |
+
assert_resolves(f"font-size: {size}", {"font-size": resolved}, inherited=inherited)
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
@pytest.mark.parametrize(
|
| 258 |
+
"size,relative_to,resolved",
|
| 259 |
+
[
|
| 260 |
+
("1em", None, "12pt"),
|
| 261 |
+
("1.0em", None, "12pt"),
|
| 262 |
+
("1.25em", None, "15pt"),
|
| 263 |
+
("1em", "16pt", "16pt"),
|
| 264 |
+
("1.0em", "16pt", "16pt"),
|
| 265 |
+
("1.25em", "16pt", "20pt"),
|
| 266 |
+
("1rem", "16pt", "12pt"),
|
| 267 |
+
("1.0rem", "16pt", "12pt"),
|
| 268 |
+
("1.25rem", "16pt", "15pt"),
|
| 269 |
+
("100%", None, "12pt"),
|
| 270 |
+
("125%", None, "15pt"),
|
| 271 |
+
("100%", "16pt", "16pt"),
|
| 272 |
+
("125%", "16pt", "20pt"),
|
| 273 |
+
("2ex", None, "12pt"),
|
| 274 |
+
("2.0ex", None, "12pt"),
|
| 275 |
+
("2.50ex", None, "15pt"),
|
| 276 |
+
("inherit", "16pt", "16pt"),
|
| 277 |
+
("smaller", None, "10pt"),
|
| 278 |
+
("smaller", "18pt", "15pt"),
|
| 279 |
+
("larger", None, f"{14.4:f}pt"),
|
| 280 |
+
("larger", "15pt", "18pt"),
|
| 281 |
+
],
|
| 282 |
+
)
|
| 283 |
+
def test_css_relative_font_size(size, relative_to, resolved):
|
| 284 |
+
if relative_to is None:
|
| 285 |
+
inherited = None
|
| 286 |
+
else:
|
| 287 |
+
inherited = {"font-size": relative_to}
|
| 288 |
+
assert_resolves(f"font-size: {size}", {"font-size": resolved}, inherited=inherited)
|
.venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_eng_formatting.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pytest
|
| 3 |
+
|
| 4 |
+
from pandas import (
|
| 5 |
+
DataFrame,
|
| 6 |
+
reset_option,
|
| 7 |
+
set_eng_float_format,
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
from pandas.io.formats.format import EngFormatter
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@pytest.fixture(autouse=True)
|
| 14 |
+
def reset_float_format():
|
| 15 |
+
yield
|
| 16 |
+
reset_option("display.float_format")
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class TestEngFormatter:
|
| 20 |
+
def test_eng_float_formatter2(self, float_frame):
|
| 21 |
+
df = float_frame
|
| 22 |
+
df.loc[5] = 0
|
| 23 |
+
|
| 24 |
+
set_eng_float_format()
|
| 25 |
+
repr(df)
|
| 26 |
+
|
| 27 |
+
set_eng_float_format(use_eng_prefix=True)
|
| 28 |
+
repr(df)
|
| 29 |
+
|
| 30 |
+
set_eng_float_format(accuracy=0)
|
| 31 |
+
repr(df)
|
| 32 |
+
|
| 33 |
+
def test_eng_float_formatter(self):
|
| 34 |
+
df = DataFrame({"A": [1.41, 141.0, 14100, 1410000.0]})
|
| 35 |
+
|
| 36 |
+
set_eng_float_format()
|
| 37 |
+
result = df.to_string()
|
| 38 |
+
expected = (
|
| 39 |
+
" A\n"
|
| 40 |
+
"0 1.410E+00\n"
|
| 41 |
+
"1 141.000E+00\n"
|
| 42 |
+
"2 14.100E+03\n"
|
| 43 |
+
"3 1.410E+06"
|
| 44 |
+
)
|
| 45 |
+
assert result == expected
|
| 46 |
+
|
| 47 |
+
set_eng_float_format(use_eng_prefix=True)
|
| 48 |
+
result = df.to_string()
|
| 49 |
+
expected = " A\n0 1.410\n1 141.000\n2 14.100k\n3 1.410M"
|
| 50 |
+
assert result == expected
|
| 51 |
+
|
| 52 |
+
set_eng_float_format(accuracy=0)
|
| 53 |
+
result = df.to_string()
|
| 54 |
+
expected = " A\n0 1E+00\n1 141E+00\n2 14E+03\n3 1E+06"
|
| 55 |
+
assert result == expected
|
| 56 |
+
|
| 57 |
+
def compare(self, formatter, input, output):
|
| 58 |
+
formatted_input = formatter(input)
|
| 59 |
+
assert formatted_input == output
|
| 60 |
+
|
| 61 |
+
def compare_all(self, formatter, in_out):
|
| 62 |
+
"""
|
| 63 |
+
Parameters:
|
| 64 |
+
-----------
|
| 65 |
+
formatter: EngFormatter under test
|
| 66 |
+
in_out: list of tuples. Each tuple = (number, expected_formatting)
|
| 67 |
+
|
| 68 |
+
It is tested if 'formatter(number) == expected_formatting'.
|
| 69 |
+
*number* should be >= 0 because formatter(-number) == fmt is also
|
| 70 |
+
tested. *fmt* is derived from *expected_formatting*
|
| 71 |
+
"""
|
| 72 |
+
for input, output in in_out:
|
| 73 |
+
self.compare(formatter, input, output)
|
| 74 |
+
self.compare(formatter, -input, "-" + output[1:])
|
| 75 |
+
|
| 76 |
+
def test_exponents_with_eng_prefix(self):
|
| 77 |
+
formatter = EngFormatter(accuracy=3, use_eng_prefix=True)
|
| 78 |
+
f = np.sqrt(2)
|
| 79 |
+
in_out = [
|
| 80 |
+
(f * 10**-24, " 1.414y"),
|
| 81 |
+
(f * 10**-23, " 14.142y"),
|
| 82 |
+
(f * 10**-22, " 141.421y"),
|
| 83 |
+
(f * 10**-21, " 1.414z"),
|
| 84 |
+
(f * 10**-20, " 14.142z"),
|
| 85 |
+
(f * 10**-19, " 141.421z"),
|
| 86 |
+
(f * 10**-18, " 1.414a"),
|
| 87 |
+
(f * 10**-17, " 14.142a"),
|
| 88 |
+
(f * 10**-16, " 141.421a"),
|
| 89 |
+
(f * 10**-15, " 1.414f"),
|
| 90 |
+
(f * 10**-14, " 14.142f"),
|
| 91 |
+
(f * 10**-13, " 141.421f"),
|
| 92 |
+
(f * 10**-12, " 1.414p"),
|
| 93 |
+
(f * 10**-11, " 14.142p"),
|
| 94 |
+
(f * 10**-10, " 141.421p"),
|
| 95 |
+
(f * 10**-9, " 1.414n"),
|
| 96 |
+
(f * 10**-8, " 14.142n"),
|
| 97 |
+
(f * 10**-7, " 141.421n"),
|
| 98 |
+
(f * 10**-6, " 1.414u"),
|
| 99 |
+
(f * 10**-5, " 14.142u"),
|
| 100 |
+
(f * 10**-4, " 141.421u"),
|
| 101 |
+
(f * 10**-3, " 1.414m"),
|
| 102 |
+
(f * 10**-2, " 14.142m"),
|
| 103 |
+
(f * 10**-1, " 141.421m"),
|
| 104 |
+
(f * 10**0, " 1.414"),
|
| 105 |
+
(f * 10**1, " 14.142"),
|
| 106 |
+
(f * 10**2, " 141.421"),
|
| 107 |
+
(f * 10**3, " 1.414k"),
|
| 108 |
+
(f * 10**4, " 14.142k"),
|
| 109 |
+
(f * 10**5, " 141.421k"),
|
| 110 |
+
(f * 10**6, " 1.414M"),
|
| 111 |
+
(f * 10**7, " 14.142M"),
|
| 112 |
+
(f * 10**8, " 141.421M"),
|
| 113 |
+
(f * 10**9, " 1.414G"),
|
| 114 |
+
(f * 10**10, " 14.142G"),
|
| 115 |
+
(f * 10**11, " 141.421G"),
|
| 116 |
+
(f * 10**12, " 1.414T"),
|
| 117 |
+
(f * 10**13, " 14.142T"),
|
| 118 |
+
(f * 10**14, " 141.421T"),
|
| 119 |
+
(f * 10**15, " 1.414P"),
|
| 120 |
+
(f * 10**16, " 14.142P"),
|
| 121 |
+
(f * 10**17, " 141.421P"),
|
| 122 |
+
(f * 10**18, " 1.414E"),
|
| 123 |
+
(f * 10**19, " 14.142E"),
|
| 124 |
+
(f * 10**20, " 141.421E"),
|
| 125 |
+
(f * 10**21, " 1.414Z"),
|
| 126 |
+
(f * 10**22, " 14.142Z"),
|
| 127 |
+
(f * 10**23, " 141.421Z"),
|
| 128 |
+
(f * 10**24, " 1.414Y"),
|
| 129 |
+
(f * 10**25, " 14.142Y"),
|
| 130 |
+
(f * 10**26, " 141.421Y"),
|
| 131 |
+
]
|
| 132 |
+
self.compare_all(formatter, in_out)
|
| 133 |
+
|
| 134 |
+
def test_exponents_without_eng_prefix(self):
|
| 135 |
+
formatter = EngFormatter(accuracy=4, use_eng_prefix=False)
|
| 136 |
+
f = np.pi
|
| 137 |
+
in_out = [
|
| 138 |
+
(f * 10**-24, " 3.1416E-24"),
|
| 139 |
+
(f * 10**-23, " 31.4159E-24"),
|
| 140 |
+
(f * 10**-22, " 314.1593E-24"),
|
| 141 |
+
(f * 10**-21, " 3.1416E-21"),
|
| 142 |
+
(f * 10**-20, " 31.4159E-21"),
|
| 143 |
+
(f * 10**-19, " 314.1593E-21"),
|
| 144 |
+
(f * 10**-18, " 3.1416E-18"),
|
| 145 |
+
(f * 10**-17, " 31.4159E-18"),
|
| 146 |
+
(f * 10**-16, " 314.1593E-18"),
|
| 147 |
+
(f * 10**-15, " 3.1416E-15"),
|
| 148 |
+
(f * 10**-14, " 31.4159E-15"),
|
| 149 |
+
(f * 10**-13, " 314.1593E-15"),
|
| 150 |
+
(f * 10**-12, " 3.1416E-12"),
|
| 151 |
+
(f * 10**-11, " 31.4159E-12"),
|
| 152 |
+
(f * 10**-10, " 314.1593E-12"),
|
| 153 |
+
(f * 10**-9, " 3.1416E-09"),
|
| 154 |
+
(f * 10**-8, " 31.4159E-09"),
|
| 155 |
+
(f * 10**-7, " 314.1593E-09"),
|
| 156 |
+
(f * 10**-6, " 3.1416E-06"),
|
| 157 |
+
(f * 10**-5, " 31.4159E-06"),
|
| 158 |
+
(f * 10**-4, " 314.1593E-06"),
|
| 159 |
+
(f * 10**-3, " 3.1416E-03"),
|
| 160 |
+
(f * 10**-2, " 31.4159E-03"),
|
| 161 |
+
(f * 10**-1, " 314.1593E-03"),
|
| 162 |
+
(f * 10**0, " 3.1416E+00"),
|
| 163 |
+
(f * 10**1, " 31.4159E+00"),
|
| 164 |
+
(f * 10**2, " 314.1593E+00"),
|
| 165 |
+
(f * 10**3, " 3.1416E+03"),
|
| 166 |
+
(f * 10**4, " 31.4159E+03"),
|
| 167 |
+
(f * 10**5, " 314.1593E+03"),
|
| 168 |
+
(f * 10**6, " 3.1416E+06"),
|
| 169 |
+
(f * 10**7, " 31.4159E+06"),
|
| 170 |
+
(f * 10**8, " 314.1593E+06"),
|
| 171 |
+
(f * 10**9, " 3.1416E+09"),
|
| 172 |
+
(f * 10**10, " 31.4159E+09"),
|
| 173 |
+
(f * 10**11, " 314.1593E+09"),
|
| 174 |
+
(f * 10**12, " 3.1416E+12"),
|
| 175 |
+
(f * 10**13, " 31.4159E+12"),
|
| 176 |
+
(f * 10**14, " 314.1593E+12"),
|
| 177 |
+
(f * 10**15, " 3.1416E+15"),
|
| 178 |
+
(f * 10**16, " 31.4159E+15"),
|
| 179 |
+
(f * 10**17, " 314.1593E+15"),
|
| 180 |
+
(f * 10**18, " 3.1416E+18"),
|
| 181 |
+
(f * 10**19, " 31.4159E+18"),
|
| 182 |
+
(f * 10**20, " 314.1593E+18"),
|
| 183 |
+
(f * 10**21, " 3.1416E+21"),
|
| 184 |
+
(f * 10**22, " 31.4159E+21"),
|
| 185 |
+
(f * 10**23, " 314.1593E+21"),
|
| 186 |
+
(f * 10**24, " 3.1416E+24"),
|
| 187 |
+
(f * 10**25, " 31.4159E+24"),
|
| 188 |
+
(f * 10**26, " 314.1593E+24"),
|
| 189 |
+
]
|
| 190 |
+
self.compare_all(formatter, in_out)
|
| 191 |
+
|
| 192 |
+
def test_rounding(self):
|
| 193 |
+
formatter = EngFormatter(accuracy=3, use_eng_prefix=True)
|
| 194 |
+
in_out = [
|
| 195 |
+
(5.55555, " 5.556"),
|
| 196 |
+
(55.5555, " 55.556"),
|
| 197 |
+
(555.555, " 555.555"),
|
| 198 |
+
(5555.55, " 5.556k"),
|
| 199 |
+
(55555.5, " 55.556k"),
|
| 200 |
+
(555555, " 555.555k"),
|
| 201 |
+
]
|
| 202 |
+
self.compare_all(formatter, in_out)
|
| 203 |
+
|
| 204 |
+
formatter = EngFormatter(accuracy=1, use_eng_prefix=True)
|
| 205 |
+
in_out = [
|
| 206 |
+
(5.55555, " 5.6"),
|
| 207 |
+
(55.5555, " 55.6"),
|
| 208 |
+
(555.555, " 555.6"),
|
| 209 |
+
(5555.55, " 5.6k"),
|
| 210 |
+
(55555.5, " 55.6k"),
|
| 211 |
+
(555555, " 555.6k"),
|
| 212 |
+
]
|
| 213 |
+
self.compare_all(formatter, in_out)
|
| 214 |
+
|
| 215 |
+
formatter = EngFormatter(accuracy=0, use_eng_prefix=True)
|
| 216 |
+
in_out = [
|
| 217 |
+
(5.55555, " 6"),
|
| 218 |
+
(55.5555, " 56"),
|
| 219 |
+
(555.555, " 556"),
|
| 220 |
+
(5555.55, " 6k"),
|
| 221 |
+
(55555.5, " 56k"),
|
| 222 |
+
(555555, " 556k"),
|
| 223 |
+
]
|
| 224 |
+
self.compare_all(formatter, in_out)
|
| 225 |
+
|
| 226 |
+
formatter = EngFormatter(accuracy=3, use_eng_prefix=True)
|
| 227 |
+
result = formatter(0)
|
| 228 |
+
assert result == " 0.000"
|
| 229 |
+
|
| 230 |
+
def test_nan(self):
|
| 231 |
+
# Issue #11981
|
| 232 |
+
|
| 233 |
+
formatter = EngFormatter(accuracy=1, use_eng_prefix=True)
|
| 234 |
+
result = formatter(np.nan)
|
| 235 |
+
assert result == "NaN"
|
| 236 |
+
|
| 237 |
+
df = DataFrame(
|
| 238 |
+
{
|
| 239 |
+
"a": [1.5, 10.3, 20.5],
|
| 240 |
+
"b": [50.3, 60.67, 70.12],
|
| 241 |
+
"c": [100.2, 101.33, 120.33],
|
| 242 |
+
}
|
| 243 |
+
)
|
| 244 |
+
pt = df.pivot_table(values="a", index="b", columns="c")
|
| 245 |
+
set_eng_float_format(accuracy=1)
|
| 246 |
+
result = pt.to_string()
|
| 247 |
+
assert "NaN" in result
|
| 248 |
+
|
| 249 |
+
def test_inf(self):
|
| 250 |
+
# Issue #11981
|
| 251 |
+
|
| 252 |
+
formatter = EngFormatter(accuracy=1, use_eng_prefix=True)
|
| 253 |
+
result = formatter(np.inf)
|
| 254 |
+
assert result == "inf"
|
.venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_format.py
ADDED
|
@@ -0,0 +1,2302 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tests for the file pandas.io.formats.format, *not* tests for general formatting
|
| 3 |
+
of pandas objects.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from io import StringIO
|
| 8 |
+
import re
|
| 9 |
+
from shutil import get_terminal_size
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
import pytest
|
| 13 |
+
|
| 14 |
+
import pandas as pd
|
| 15 |
+
from pandas import (
|
| 16 |
+
DataFrame,
|
| 17 |
+
Index,
|
| 18 |
+
MultiIndex,
|
| 19 |
+
NaT,
|
| 20 |
+
Series,
|
| 21 |
+
Timestamp,
|
| 22 |
+
date_range,
|
| 23 |
+
get_option,
|
| 24 |
+
option_context,
|
| 25 |
+
read_csv,
|
| 26 |
+
reset_option,
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
from pandas.io.formats import printing
|
| 30 |
+
import pandas.io.formats.format as fmt
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def has_info_repr(df):
|
| 34 |
+
r = repr(df)
|
| 35 |
+
c1 = r.split("\n")[0].startswith("<class")
|
| 36 |
+
c2 = r.split("\n")[0].startswith(r"<class") # _repr_html_
|
| 37 |
+
return c1 or c2
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def has_non_verbose_info_repr(df):
|
| 41 |
+
has_info = has_info_repr(df)
|
| 42 |
+
r = repr(df)
|
| 43 |
+
|
| 44 |
+
# 1. <class>
|
| 45 |
+
# 2. Index
|
| 46 |
+
# 3. Columns
|
| 47 |
+
# 4. dtype
|
| 48 |
+
# 5. memory usage
|
| 49 |
+
# 6. trailing newline
|
| 50 |
+
nv = len(r.split("\n")) == 6
|
| 51 |
+
return has_info and nv
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def has_horizontally_truncated_repr(df):
|
| 55 |
+
try: # Check header row
|
| 56 |
+
fst_line = np.array(repr(df).splitlines()[0].split())
|
| 57 |
+
cand_col = np.where(fst_line == "...")[0][0]
|
| 58 |
+
except IndexError:
|
| 59 |
+
return False
|
| 60 |
+
# Make sure each row has this ... in the same place
|
| 61 |
+
r = repr(df)
|
| 62 |
+
for ix, _ in enumerate(r.splitlines()):
|
| 63 |
+
if not r.split()[cand_col] == "...":
|
| 64 |
+
return False
|
| 65 |
+
return True
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def has_vertically_truncated_repr(df):
|
| 69 |
+
r = repr(df)
|
| 70 |
+
only_dot_row = False
|
| 71 |
+
for row in r.splitlines():
|
| 72 |
+
if re.match(r"^[\.\ ]+$", row):
|
| 73 |
+
only_dot_row = True
|
| 74 |
+
return only_dot_row
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def has_truncated_repr(df):
|
| 78 |
+
return has_horizontally_truncated_repr(df) or has_vertically_truncated_repr(df)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def has_doubly_truncated_repr(df):
|
| 82 |
+
return has_horizontally_truncated_repr(df) and has_vertically_truncated_repr(df)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def has_expanded_repr(df):
|
| 86 |
+
r = repr(df)
|
| 87 |
+
for line in r.split("\n"):
|
| 88 |
+
if line.endswith("\\"):
|
| 89 |
+
return True
|
| 90 |
+
return False
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class TestDataFrameFormatting:
|
| 94 |
+
def test_repr_truncation(self):
|
| 95 |
+
max_len = 20
|
| 96 |
+
with option_context("display.max_colwidth", max_len):
|
| 97 |
+
df = DataFrame(
|
| 98 |
+
{
|
| 99 |
+
"A": np.random.default_rng(2).standard_normal(10),
|
| 100 |
+
"B": [
|
| 101 |
+
"a"
|
| 102 |
+
* np.random.default_rng(2).integers(max_len - 1, max_len + 1)
|
| 103 |
+
for _ in range(10)
|
| 104 |
+
],
|
| 105 |
+
}
|
| 106 |
+
)
|
| 107 |
+
r = repr(df)
|
| 108 |
+
r = r[r.find("\n") + 1 :]
|
| 109 |
+
|
| 110 |
+
adj = printing.get_adjustment()
|
| 111 |
+
|
| 112 |
+
for line, value in zip(r.split("\n"), df["B"]):
|
| 113 |
+
if adj.len(value) + 1 > max_len:
|
| 114 |
+
assert "..." in line
|
| 115 |
+
else:
|
| 116 |
+
assert "..." not in line
|
| 117 |
+
|
| 118 |
+
with option_context("display.max_colwidth", 999999):
|
| 119 |
+
assert "..." not in repr(df)
|
| 120 |
+
|
| 121 |
+
with option_context("display.max_colwidth", max_len + 2):
|
| 122 |
+
assert "..." not in repr(df)
|
| 123 |
+
|
| 124 |
+
def test_repr_truncation_preserves_na(self):
|
| 125 |
+
# https://github.com/pandas-dev/pandas/issues/55630
|
| 126 |
+
df = DataFrame({"a": [pd.NA for _ in range(10)]})
|
| 127 |
+
with option_context("display.max_rows", 2, "display.show_dimensions", False):
|
| 128 |
+
assert repr(df) == " a\n0 <NA>\n.. ...\n9 <NA>"
|
| 129 |
+
|
| 130 |
+
def test_repr_truncation_dataframe_attrs(self):
|
| 131 |
+
# GH#60455
|
| 132 |
+
df = DataFrame([[0] * 10])
|
| 133 |
+
df.attrs["b"] = DataFrame([])
|
| 134 |
+
with option_context("display.max_columns", 2, "display.show_dimensions", False):
|
| 135 |
+
assert repr(df) == " 0 ... 9\n0 0 ... 0"
|
| 136 |
+
|
| 137 |
+
def test_repr_truncation_series_with_dataframe_attrs(self):
|
| 138 |
+
# GH#60568
|
| 139 |
+
ser = Series([0] * 10)
|
| 140 |
+
ser.attrs["b"] = DataFrame([])
|
| 141 |
+
with option_context("display.max_rows", 2, "display.show_dimensions", False):
|
| 142 |
+
assert repr(ser) == "0 0\n ..\n9 0\ndtype: int64"
|
| 143 |
+
|
| 144 |
+
def test_max_colwidth_negative_int_raises(self):
|
| 145 |
+
# Deprecation enforced from:
|
| 146 |
+
# https://github.com/pandas-dev/pandas/issues/31532
|
| 147 |
+
with pytest.raises(
|
| 148 |
+
ValueError, match="Value must be a nonnegative integer or None"
|
| 149 |
+
):
|
| 150 |
+
with option_context("display.max_colwidth", -1):
|
| 151 |
+
pass
|
| 152 |
+
|
| 153 |
+
def test_repr_chop_threshold(self):
|
| 154 |
+
df = DataFrame([[0.1, 0.5], [0.5, -0.1]])
|
| 155 |
+
reset_option("display.chop_threshold") # default None
|
| 156 |
+
assert repr(df) == " 0 1\n0 0.1 0.5\n1 0.5 -0.1"
|
| 157 |
+
|
| 158 |
+
with option_context("display.chop_threshold", 0.2):
|
| 159 |
+
assert repr(df) == " 0 1\n0 0.0 0.5\n1 0.5 0.0"
|
| 160 |
+
|
| 161 |
+
with option_context("display.chop_threshold", 0.6):
|
| 162 |
+
assert repr(df) == " 0 1\n0 0.0 0.0\n1 0.0 0.0"
|
| 163 |
+
|
| 164 |
+
with option_context("display.chop_threshold", None):
|
| 165 |
+
assert repr(df) == " 0 1\n0 0.1 0.5\n1 0.5 -0.1"
|
| 166 |
+
|
| 167 |
+
def test_repr_chop_threshold_column_below(self):
|
| 168 |
+
# GH 6839: validation case
|
| 169 |
+
|
| 170 |
+
df = DataFrame([[10, 20, 30, 40], [8e-10, -1e-11, 2e-9, -2e-11]]).T
|
| 171 |
+
|
| 172 |
+
with option_context("display.chop_threshold", 0):
|
| 173 |
+
assert repr(df) == (
|
| 174 |
+
" 0 1\n"
|
| 175 |
+
"0 10.0 8.000000e-10\n"
|
| 176 |
+
"1 20.0 -1.000000e-11\n"
|
| 177 |
+
"2 30.0 2.000000e-09\n"
|
| 178 |
+
"3 40.0 -2.000000e-11"
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
with option_context("display.chop_threshold", 1e-8):
|
| 182 |
+
assert repr(df) == (
|
| 183 |
+
" 0 1\n"
|
| 184 |
+
"0 10.0 0.000000e+00\n"
|
| 185 |
+
"1 20.0 0.000000e+00\n"
|
| 186 |
+
"2 30.0 0.000000e+00\n"
|
| 187 |
+
"3 40.0 0.000000e+00"
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
with option_context("display.chop_threshold", 5e-11):
|
| 191 |
+
assert repr(df) == (
|
| 192 |
+
" 0 1\n"
|
| 193 |
+
"0 10.0 8.000000e-10\n"
|
| 194 |
+
"1 20.0 0.000000e+00\n"
|
| 195 |
+
"2 30.0 2.000000e-09\n"
|
| 196 |
+
"3 40.0 0.000000e+00"
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
def test_repr_no_backslash(self):
|
| 200 |
+
with option_context("mode.sim_interactive", True):
|
| 201 |
+
df = DataFrame(np.random.default_rng(2).standard_normal((10, 4)))
|
| 202 |
+
assert "\\" not in repr(df)
|
| 203 |
+
|
| 204 |
+
def test_expand_frame_repr(self):
|
| 205 |
+
df_small = DataFrame("hello", index=[0], columns=[0])
|
| 206 |
+
df_wide = DataFrame("hello", index=[0], columns=range(10))
|
| 207 |
+
df_tall = DataFrame("hello", index=range(30), columns=range(5))
|
| 208 |
+
|
| 209 |
+
with option_context("mode.sim_interactive", True):
|
| 210 |
+
with option_context(
|
| 211 |
+
"display.max_columns",
|
| 212 |
+
10,
|
| 213 |
+
"display.width",
|
| 214 |
+
20,
|
| 215 |
+
"display.max_rows",
|
| 216 |
+
20,
|
| 217 |
+
"display.show_dimensions",
|
| 218 |
+
True,
|
| 219 |
+
):
|
| 220 |
+
with option_context("display.expand_frame_repr", True):
|
| 221 |
+
assert not has_truncated_repr(df_small)
|
| 222 |
+
assert not has_expanded_repr(df_small)
|
| 223 |
+
assert not has_truncated_repr(df_wide)
|
| 224 |
+
assert has_expanded_repr(df_wide)
|
| 225 |
+
assert has_vertically_truncated_repr(df_tall)
|
| 226 |
+
assert has_expanded_repr(df_tall)
|
| 227 |
+
|
| 228 |
+
with option_context("display.expand_frame_repr", False):
|
| 229 |
+
assert not has_truncated_repr(df_small)
|
| 230 |
+
assert not has_expanded_repr(df_small)
|
| 231 |
+
assert not has_horizontally_truncated_repr(df_wide)
|
| 232 |
+
assert not has_expanded_repr(df_wide)
|
| 233 |
+
assert has_vertically_truncated_repr(df_tall)
|
| 234 |
+
assert not has_expanded_repr(df_tall)
|
| 235 |
+
|
| 236 |
+
def test_repr_non_interactive(self):
|
| 237 |
+
# in non interactive mode, there can be no dependency on the
|
| 238 |
+
# result of terminal auto size detection
|
| 239 |
+
df = DataFrame("hello", index=range(1000), columns=range(5))
|
| 240 |
+
|
| 241 |
+
with option_context(
|
| 242 |
+
"mode.sim_interactive", False, "display.width", 0, "display.max_rows", 5000
|
| 243 |
+
):
|
| 244 |
+
assert not has_truncated_repr(df)
|
| 245 |
+
assert not has_expanded_repr(df)
|
| 246 |
+
|
| 247 |
+
def test_repr_truncates_terminal_size(self, monkeypatch):
|
| 248 |
+
# see gh-21180
|
| 249 |
+
|
| 250 |
+
terminal_size = (118, 96)
|
| 251 |
+
monkeypatch.setattr(
|
| 252 |
+
"pandas.io.formats.format.get_terminal_size", lambda: terminal_size
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
index = range(5)
|
| 256 |
+
columns = MultiIndex.from_tuples(
|
| 257 |
+
[
|
| 258 |
+
("This is a long title with > 37 chars.", "cat"),
|
| 259 |
+
("This is a loooooonger title with > 43 chars.", "dog"),
|
| 260 |
+
]
|
| 261 |
+
)
|
| 262 |
+
df = DataFrame(1, index=index, columns=columns)
|
| 263 |
+
|
| 264 |
+
result = repr(df)
|
| 265 |
+
|
| 266 |
+
h1, h2 = result.split("\n")[:2]
|
| 267 |
+
assert "long" in h1
|
| 268 |
+
assert "loooooonger" in h1
|
| 269 |
+
assert "cat" in h2
|
| 270 |
+
assert "dog" in h2
|
| 271 |
+
|
| 272 |
+
# regular columns
|
| 273 |
+
df2 = DataFrame({"A" * 41: [1, 2], "B" * 41: [1, 2]})
|
| 274 |
+
result = repr(df2)
|
| 275 |
+
|
| 276 |
+
assert df2.columns[0] in result.split("\n")[0]
|
| 277 |
+
|
| 278 |
+
def test_repr_truncates_terminal_size_full(self, monkeypatch):
|
| 279 |
+
# GH 22984 ensure entire window is filled
|
| 280 |
+
terminal_size = (80, 24)
|
| 281 |
+
df = DataFrame(np.random.default_rng(2).random((1, 7)))
|
| 282 |
+
|
| 283 |
+
monkeypatch.setattr(
|
| 284 |
+
"pandas.io.formats.format.get_terminal_size", lambda: terminal_size
|
| 285 |
+
)
|
| 286 |
+
assert "..." not in str(df)
|
| 287 |
+
|
| 288 |
+
def test_repr_truncation_column_size(self):
|
| 289 |
+
# dataframe with last column very wide -> check it is not used to
|
| 290 |
+
# determine size of truncation (...) column
|
| 291 |
+
df = DataFrame(
|
| 292 |
+
{
|
| 293 |
+
"a": [108480, 30830],
|
| 294 |
+
"b": [12345, 12345],
|
| 295 |
+
"c": [12345, 12345],
|
| 296 |
+
"d": [12345, 12345],
|
| 297 |
+
"e": ["a" * 50] * 2,
|
| 298 |
+
}
|
| 299 |
+
)
|
| 300 |
+
assert "..." in str(df)
|
| 301 |
+
assert " ... " not in str(df)
|
| 302 |
+
|
| 303 |
+
def test_repr_max_columns_max_rows(self):
|
| 304 |
+
term_width, term_height = get_terminal_size()
|
| 305 |
+
if term_width < 10 or term_height < 10:
|
| 306 |
+
pytest.skip(f"terminal size too small, {term_width} x {term_height}")
|
| 307 |
+
|
| 308 |
+
def mkframe(n):
|
| 309 |
+
index = [f"{i:05d}" for i in range(n)]
|
| 310 |
+
return DataFrame(0, index, index)
|
| 311 |
+
|
| 312 |
+
df6 = mkframe(6)
|
| 313 |
+
df10 = mkframe(10)
|
| 314 |
+
with option_context("mode.sim_interactive", True):
|
| 315 |
+
with option_context("display.width", term_width * 2):
|
| 316 |
+
with option_context("display.max_rows", 5, "display.max_columns", 5):
|
| 317 |
+
assert not has_expanded_repr(mkframe(4))
|
| 318 |
+
assert not has_expanded_repr(mkframe(5))
|
| 319 |
+
assert not has_expanded_repr(df6)
|
| 320 |
+
assert has_doubly_truncated_repr(df6)
|
| 321 |
+
|
| 322 |
+
with option_context("display.max_rows", 20, "display.max_columns", 10):
|
| 323 |
+
# Out off max_columns boundary, but no extending
|
| 324 |
+
# since not exceeding width
|
| 325 |
+
assert not has_expanded_repr(df6)
|
| 326 |
+
assert not has_truncated_repr(df6)
|
| 327 |
+
|
| 328 |
+
with option_context("display.max_rows", 9, "display.max_columns", 10):
|
| 329 |
+
# out vertical bounds can not result in expanded repr
|
| 330 |
+
assert not has_expanded_repr(df10)
|
| 331 |
+
assert has_vertically_truncated_repr(df10)
|
| 332 |
+
|
| 333 |
+
# width=None in terminal, auto detection
|
| 334 |
+
with option_context(
|
| 335 |
+
"display.max_columns",
|
| 336 |
+
100,
|
| 337 |
+
"display.max_rows",
|
| 338 |
+
term_width * 20,
|
| 339 |
+
"display.width",
|
| 340 |
+
None,
|
| 341 |
+
):
|
| 342 |
+
df = mkframe((term_width // 7) - 2)
|
| 343 |
+
assert not has_expanded_repr(df)
|
| 344 |
+
df = mkframe((term_width // 7) + 2)
|
| 345 |
+
printing.pprint_thing(df._repr_fits_horizontal_())
|
| 346 |
+
assert has_expanded_repr(df)
|
| 347 |
+
|
| 348 |
+
def test_repr_min_rows(self):
|
| 349 |
+
df = DataFrame({"a": range(20)})
|
| 350 |
+
|
| 351 |
+
# default setting no truncation even if above min_rows
|
| 352 |
+
assert ".." not in repr(df)
|
| 353 |
+
assert ".." not in df._repr_html_()
|
| 354 |
+
|
| 355 |
+
df = DataFrame({"a": range(61)})
|
| 356 |
+
|
| 357 |
+
# default of max_rows 60 triggers truncation if above
|
| 358 |
+
assert ".." in repr(df)
|
| 359 |
+
assert ".." in df._repr_html_()
|
| 360 |
+
|
| 361 |
+
with option_context("display.max_rows", 10, "display.min_rows", 4):
|
| 362 |
+
# truncated after first two rows
|
| 363 |
+
assert ".." in repr(df)
|
| 364 |
+
assert "2 " not in repr(df)
|
| 365 |
+
assert "..." in df._repr_html_()
|
| 366 |
+
assert "<td>2</td>" not in df._repr_html_()
|
| 367 |
+
|
| 368 |
+
with option_context("display.max_rows", 12, "display.min_rows", None):
|
| 369 |
+
# when set to None, follow value of max_rows
|
| 370 |
+
assert "5 5" in repr(df)
|
| 371 |
+
assert "<td>5</td>" in df._repr_html_()
|
| 372 |
+
|
| 373 |
+
with option_context("display.max_rows", 10, "display.min_rows", 12):
|
| 374 |
+
# when set value higher as max_rows, use the minimum
|
| 375 |
+
assert "5 5" not in repr(df)
|
| 376 |
+
assert "<td>5</td>" not in df._repr_html_()
|
| 377 |
+
|
| 378 |
+
with option_context("display.max_rows", None, "display.min_rows", 12):
|
| 379 |
+
# max_rows of None -> never truncate
|
| 380 |
+
assert ".." not in repr(df)
|
| 381 |
+
assert ".." not in df._repr_html_()
|
| 382 |
+
|
| 383 |
+
@pytest.mark.parametrize(
|
| 384 |
+
"data, format_option, expected_values",
|
| 385 |
+
[
|
| 386 |
+
(12345.6789, "{:12.3f}", "12345.679"),
|
| 387 |
+
(None, "{:.3f}", "None"),
|
| 388 |
+
("", "{:.2f}", ""),
|
| 389 |
+
(112345.6789, "{:6.3f}", "112345.679"),
|
| 390 |
+
("foo foo", None, "foo foo"),
|
| 391 |
+
(" foo", None, "foo"),
|
| 392 |
+
(
|
| 393 |
+
"foo foo foo",
|
| 394 |
+
None,
|
| 395 |
+
"foo foo foo",
|
| 396 |
+
), # odd no.of spaces
|
| 397 |
+
(
|
| 398 |
+
"foo foo foo",
|
| 399 |
+
None,
|
| 400 |
+
"foo foo foo",
|
| 401 |
+
), # even no.of spaces
|
| 402 |
+
],
|
| 403 |
+
)
|
| 404 |
+
def test_repr_float_formatting_html_output(
|
| 405 |
+
self, data, format_option, expected_values
|
| 406 |
+
):
|
| 407 |
+
if format_option is not None:
|
| 408 |
+
with option_context("display.float_format", format_option.format):
|
| 409 |
+
df = DataFrame({"A": [data]})
|
| 410 |
+
html_output = df._repr_html_()
|
| 411 |
+
assert expected_values in html_output
|
| 412 |
+
else:
|
| 413 |
+
df = DataFrame({"A": [data]})
|
| 414 |
+
html_output = df._repr_html_()
|
| 415 |
+
assert expected_values in html_output
|
| 416 |
+
|
| 417 |
+
def test_str_max_colwidth(self):
|
| 418 |
+
# GH 7856
|
| 419 |
+
df = DataFrame(
|
| 420 |
+
[
|
| 421 |
+
{
|
| 422 |
+
"a": "foo",
|
| 423 |
+
"b": "bar",
|
| 424 |
+
"c": "uncomfortably long line with lots of stuff",
|
| 425 |
+
"d": 1,
|
| 426 |
+
},
|
| 427 |
+
{"a": "foo", "b": "bar", "c": "stuff", "d": 1},
|
| 428 |
+
]
|
| 429 |
+
)
|
| 430 |
+
df.set_index(["a", "b", "c"])
|
| 431 |
+
assert str(df) == (
|
| 432 |
+
" a b c d\n"
|
| 433 |
+
"0 foo bar uncomfortably long line with lots of stuff 1\n"
|
| 434 |
+
"1 foo bar stuff 1"
|
| 435 |
+
)
|
| 436 |
+
with option_context("max_colwidth", 20):
|
| 437 |
+
assert str(df) == (
|
| 438 |
+
" a b c d\n"
|
| 439 |
+
"0 foo bar uncomfortably lo... 1\n"
|
| 440 |
+
"1 foo bar stuff 1"
|
| 441 |
+
)
|
| 442 |
+
|
| 443 |
+
def test_auto_detect(self):
|
| 444 |
+
term_width, term_height = get_terminal_size()
|
| 445 |
+
fac = 1.05 # Arbitrary large factor to exceed term width
|
| 446 |
+
cols = range(int(term_width * fac))
|
| 447 |
+
index = range(10)
|
| 448 |
+
df = DataFrame(index=index, columns=cols)
|
| 449 |
+
with option_context("mode.sim_interactive", True):
|
| 450 |
+
with option_context("display.max_rows", None):
|
| 451 |
+
with option_context("display.max_columns", None):
|
| 452 |
+
# Wrap around with None
|
| 453 |
+
assert has_expanded_repr(df)
|
| 454 |
+
with option_context("display.max_rows", 0):
|
| 455 |
+
with option_context("display.max_columns", 0):
|
| 456 |
+
# Truncate with auto detection.
|
| 457 |
+
assert has_horizontally_truncated_repr(df)
|
| 458 |
+
|
| 459 |
+
index = range(int(term_height * fac))
|
| 460 |
+
df = DataFrame(index=index, columns=cols)
|
| 461 |
+
with option_context("display.max_rows", 0):
|
| 462 |
+
with option_context("display.max_columns", None):
|
| 463 |
+
# Wrap around with None
|
| 464 |
+
assert has_expanded_repr(df)
|
| 465 |
+
# Truncate vertically
|
| 466 |
+
assert has_vertically_truncated_repr(df)
|
| 467 |
+
|
| 468 |
+
with option_context("display.max_rows", None):
|
| 469 |
+
with option_context("display.max_columns", 0):
|
| 470 |
+
assert has_horizontally_truncated_repr(df)
|
| 471 |
+
|
| 472 |
+
def test_to_string_repr_unicode2(self):
|
| 473 |
+
idx = Index(["abc", "\u03c3a", "aegdvg"])
|
| 474 |
+
ser = Series(np.random.default_rng(2).standard_normal(len(idx)), idx)
|
| 475 |
+
rs = repr(ser).split("\n")
|
| 476 |
+
line_len = len(rs[0])
|
| 477 |
+
for line in rs[1:]:
|
| 478 |
+
try:
|
| 479 |
+
line = line.decode(get_option("display.encoding"))
|
| 480 |
+
except AttributeError:
|
| 481 |
+
pass
|
| 482 |
+
if not line.startswith("dtype:"):
|
| 483 |
+
assert len(line) == line_len
|
| 484 |
+
|
| 485 |
+
def test_east_asian_unicode_false(self):
|
| 486 |
+
# not aligned properly because of east asian width
|
| 487 |
+
|
| 488 |
+
# mid col
|
| 489 |
+
df = DataFrame(
|
| 490 |
+
{"a": ["あ", "いいい", "う", "ええええええ"], "b": [1, 222, 33333, 4]},
|
| 491 |
+
index=["a", "bb", "c", "ddd"],
|
| 492 |
+
)
|
| 493 |
+
expected = (
|
| 494 |
+
" a b\na あ 1\n"
|
| 495 |
+
"bb いいい 222\nc う 33333\n"
|
| 496 |
+
"ddd ええええええ 4"
|
| 497 |
+
)
|
| 498 |
+
assert repr(df) == expected
|
| 499 |
+
|
| 500 |
+
# last col
|
| 501 |
+
df = DataFrame(
|
| 502 |
+
{"a": [1, 222, 33333, 4], "b": ["あ", "いいい", "う", "ええええええ"]},
|
| 503 |
+
index=["a", "bb", "c", "ddd"],
|
| 504 |
+
)
|
| 505 |
+
expected = (
|
| 506 |
+
" a b\na 1 あ\n"
|
| 507 |
+
"bb 222 いいい\nc 33333 う\n"
|
| 508 |
+
"ddd 4 ええええええ"
|
| 509 |
+
)
|
| 510 |
+
assert repr(df) == expected
|
| 511 |
+
|
| 512 |
+
# all col
|
| 513 |
+
df = DataFrame(
|
| 514 |
+
{
|
| 515 |
+
"a": ["あああああ", "い", "う", "えええ"],
|
| 516 |
+
"b": ["あ", "いいい", "う", "ええええええ"],
|
| 517 |
+
},
|
| 518 |
+
index=["a", "bb", "c", "ddd"],
|
| 519 |
+
)
|
| 520 |
+
expected = (
|
| 521 |
+
" a b\na あああああ あ\n"
|
| 522 |
+
"bb い いいい\nc う う\n"
|
| 523 |
+
"ddd えええ ええええええ"
|
| 524 |
+
)
|
| 525 |
+
assert repr(df) == expected
|
| 526 |
+
|
| 527 |
+
# column name
|
| 528 |
+
df = DataFrame(
|
| 529 |
+
{
|
| 530 |
+
"b": ["あ", "いいい", "う", "ええええええ"],
|
| 531 |
+
"あああああ": [1, 222, 33333, 4],
|
| 532 |
+
},
|
| 533 |
+
index=["a", "bb", "c", "ddd"],
|
| 534 |
+
)
|
| 535 |
+
expected = (
|
| 536 |
+
" b あああああ\na あ 1\n"
|
| 537 |
+
"bb いいい 222\nc う 33333\n"
|
| 538 |
+
"ddd ええええええ 4"
|
| 539 |
+
)
|
| 540 |
+
assert repr(df) == expected
|
| 541 |
+
|
| 542 |
+
# index
|
| 543 |
+
df = DataFrame(
|
| 544 |
+
{
|
| 545 |
+
"a": ["あああああ", "い", "う", "えええ"],
|
| 546 |
+
"b": ["あ", "いいい", "う", "ええええええ"],
|
| 547 |
+
},
|
| 548 |
+
index=["あああ", "いいいいいい", "うう", "え"],
|
| 549 |
+
)
|
| 550 |
+
expected = (
|
| 551 |
+
" a b\nあああ あああああ あ\n"
|
| 552 |
+
"いいいいいい い いいい\nうう う う\n"
|
| 553 |
+
"え えええ ええええええ"
|
| 554 |
+
)
|
| 555 |
+
assert repr(df) == expected
|
| 556 |
+
|
| 557 |
+
# index name
|
| 558 |
+
df = DataFrame(
|
| 559 |
+
{
|
| 560 |
+
"a": ["あああああ", "い", "う", "えええ"],
|
| 561 |
+
"b": ["あ", "いいい", "う", "ええええええ"],
|
| 562 |
+
},
|
| 563 |
+
index=Index(["あ", "い", "うう", "え"], name="おおおお"),
|
| 564 |
+
)
|
| 565 |
+
expected = (
|
| 566 |
+
" a b\n"
|
| 567 |
+
"おおおお \n"
|
| 568 |
+
"あ あああああ あ\n"
|
| 569 |
+
"い い いいい\n"
|
| 570 |
+
"うう う う\n"
|
| 571 |
+
"え えええ ええええええ"
|
| 572 |
+
)
|
| 573 |
+
assert repr(df) == expected
|
| 574 |
+
|
| 575 |
+
# all
|
| 576 |
+
df = DataFrame(
|
| 577 |
+
{
|
| 578 |
+
"あああ": ["あああ", "い", "う", "えええええ"],
|
| 579 |
+
"いいいいい": ["あ", "いいい", "う", "ええ"],
|
| 580 |
+
},
|
| 581 |
+
index=Index(["あ", "いいい", "うう", "え"], name="お"),
|
| 582 |
+
)
|
| 583 |
+
expected = (
|
| 584 |
+
" あああ いいいいい\n"
|
| 585 |
+
"お \n"
|
| 586 |
+
"あ あああ あ\n"
|
| 587 |
+
"いいい い いいい\n"
|
| 588 |
+
"うう う う\n"
|
| 589 |
+
"え えええええ ええ"
|
| 590 |
+
)
|
| 591 |
+
assert repr(df) == expected
|
| 592 |
+
|
| 593 |
+
# MultiIndex
|
| 594 |
+
idx = MultiIndex.from_tuples(
|
| 595 |
+
[("あ", "いい"), ("う", "え"), ("おおお", "かかかか"), ("き", "くく")]
|
| 596 |
+
)
|
| 597 |
+
df = DataFrame(
|
| 598 |
+
{
|
| 599 |
+
"a": ["あああああ", "い", "う", "えええ"],
|
| 600 |
+
"b": ["あ", "いいい", "う", "ええええええ"],
|
| 601 |
+
},
|
| 602 |
+
index=idx,
|
| 603 |
+
)
|
| 604 |
+
expected = (
|
| 605 |
+
" a b\n"
|
| 606 |
+
"あ いい あああああ あ\n"
|
| 607 |
+
"う え い いいい\n"
|
| 608 |
+
"おおお かかかか う う\n"
|
| 609 |
+
"き くく えええ ええええええ"
|
| 610 |
+
)
|
| 611 |
+
assert repr(df) == expected
|
| 612 |
+
|
| 613 |
+
# truncate
|
| 614 |
+
with option_context("display.max_rows", 3, "display.max_columns", 3):
|
| 615 |
+
df = DataFrame(
|
| 616 |
+
{
|
| 617 |
+
"a": ["あああああ", "い", "う", "えええ"],
|
| 618 |
+
"b": ["あ", "いいい", "う", "ええええええ"],
|
| 619 |
+
"c": ["お", "か", "ききき", "くくくくくく"],
|
| 620 |
+
"ああああ": ["さ", "し", "す", "せ"],
|
| 621 |
+
},
|
| 622 |
+
columns=["a", "b", "c", "ああああ"],
|
| 623 |
+
)
|
| 624 |
+
|
| 625 |
+
expected = (
|
| 626 |
+
" a ... ああああ\n0 あああああ ... さ\n"
|
| 627 |
+
".. ... ... ...\n3 えええ ... せ\n"
|
| 628 |
+
"\n[4 rows x 4 columns]"
|
| 629 |
+
)
|
| 630 |
+
assert repr(df) == expected
|
| 631 |
+
|
| 632 |
+
df.index = ["あああ", "いいいい", "う", "aaa"]
|
| 633 |
+
expected = (
|
| 634 |
+
" a ... ああああ\nあああ あああああ ... さ\n"
|
| 635 |
+
".. ... ... ...\naaa えええ ... せ\n"
|
| 636 |
+
"\n[4 rows x 4 columns]"
|
| 637 |
+
)
|
| 638 |
+
assert repr(df) == expected
|
| 639 |
+
|
| 640 |
+
def test_east_asian_unicode_true(self):
|
| 641 |
+
# Enable Unicode option -----------------------------------------
|
| 642 |
+
with option_context("display.unicode.east_asian_width", True):
|
| 643 |
+
# mid col
|
| 644 |
+
df = DataFrame(
|
| 645 |
+
{"a": ["あ", "いいい", "う", "ええええええ"], "b": [1, 222, 33333, 4]},
|
| 646 |
+
index=["a", "bb", "c", "ddd"],
|
| 647 |
+
)
|
| 648 |
+
expected = (
|
| 649 |
+
" a b\na あ 1\n"
|
| 650 |
+
"bb いいい 222\nc う 33333\n"
|
| 651 |
+
"ddd ええええええ 4"
|
| 652 |
+
)
|
| 653 |
+
assert repr(df) == expected
|
| 654 |
+
|
| 655 |
+
# last col
|
| 656 |
+
df = DataFrame(
|
| 657 |
+
{"a": [1, 222, 33333, 4], "b": ["あ", "いいい", "う", "ええええええ"]},
|
| 658 |
+
index=["a", "bb", "c", "ddd"],
|
| 659 |
+
)
|
| 660 |
+
expected = (
|
| 661 |
+
" a b\na 1 あ\n"
|
| 662 |
+
"bb 222 いいい\nc 33333 う\n"
|
| 663 |
+
"ddd 4 ええええええ"
|
| 664 |
+
)
|
| 665 |
+
assert repr(df) == expected
|
| 666 |
+
|
| 667 |
+
# all col
|
| 668 |
+
df = DataFrame(
|
| 669 |
+
{
|
| 670 |
+
"a": ["あああああ", "い", "う", "えええ"],
|
| 671 |
+
"b": ["あ", "いいい", "う", "ええええええ"],
|
| 672 |
+
},
|
| 673 |
+
index=["a", "bb", "c", "ddd"],
|
| 674 |
+
)
|
| 675 |
+
expected = (
|
| 676 |
+
" a b\n"
|
| 677 |
+
"a あああああ あ\n"
|
| 678 |
+
"bb い いいい\n"
|
| 679 |
+
"c う う\n"
|
| 680 |
+
"ddd えええ ええええええ"
|
| 681 |
+
)
|
| 682 |
+
assert repr(df) == expected
|
| 683 |
+
|
| 684 |
+
# column name
|
| 685 |
+
df = DataFrame(
|
| 686 |
+
{
|
| 687 |
+
"b": ["あ", "いいい", "う", "ええええええ"],
|
| 688 |
+
"あああああ": [1, 222, 33333, 4],
|
| 689 |
+
},
|
| 690 |
+
index=["a", "bb", "c", "ddd"],
|
| 691 |
+
)
|
| 692 |
+
expected = (
|
| 693 |
+
" b あああああ\n"
|
| 694 |
+
"a あ 1\n"
|
| 695 |
+
"bb いいい 222\n"
|
| 696 |
+
"c う 33333\n"
|
| 697 |
+
"ddd ええええええ 4"
|
| 698 |
+
)
|
| 699 |
+
assert repr(df) == expected
|
| 700 |
+
|
| 701 |
+
# index
|
| 702 |
+
df = DataFrame(
|
| 703 |
+
{
|
| 704 |
+
"a": ["あああああ", "い", "う", "えええ"],
|
| 705 |
+
"b": ["あ", "いいい", "う", "ええええええ"],
|
| 706 |
+
},
|
| 707 |
+
index=["あああ", "いいいいいい", "うう", "え"],
|
| 708 |
+
)
|
| 709 |
+
expected = (
|
| 710 |
+
" a b\n"
|
| 711 |
+
"あああ あああああ あ\n"
|
| 712 |
+
"いいいいいい い いいい\n"
|
| 713 |
+
"うう う う\n"
|
| 714 |
+
"え えええ ええええええ"
|
| 715 |
+
)
|
| 716 |
+
assert repr(df) == expected
|
| 717 |
+
|
| 718 |
+
# index name
|
| 719 |
+
df = DataFrame(
|
| 720 |
+
{
|
| 721 |
+
"a": ["あああああ", "い", "う", "えええ"],
|
| 722 |
+
"b": ["あ", "いいい", "う", "ええええええ"],
|
| 723 |
+
},
|
| 724 |
+
index=Index(["あ", "い", "うう", "え"], name="おおおお"),
|
| 725 |
+
)
|
| 726 |
+
expected = (
|
| 727 |
+
" a b\n"
|
| 728 |
+
"おおおお \n"
|
| 729 |
+
"あ あああああ あ\n"
|
| 730 |
+
"い い いいい\n"
|
| 731 |
+
"うう う う\n"
|
| 732 |
+
"え えええ ええええええ"
|
| 733 |
+
)
|
| 734 |
+
assert repr(df) == expected
|
| 735 |
+
|
| 736 |
+
# all
|
| 737 |
+
df = DataFrame(
|
| 738 |
+
{
|
| 739 |
+
"あああ": ["あああ", "い", "う", "えええええ"],
|
| 740 |
+
"いいいいい": ["あ", "いいい", "う", "ええ"],
|
| 741 |
+
},
|
| 742 |
+
index=Index(["あ", "いいい", "うう", "え"], name="お"),
|
| 743 |
+
)
|
| 744 |
+
expected = (
|
| 745 |
+
" あああ いいいいい\n"
|
| 746 |
+
"お \n"
|
| 747 |
+
"あ あああ あ\n"
|
| 748 |
+
"いいい い いいい\n"
|
| 749 |
+
"うう う う\n"
|
| 750 |
+
"え えええええ ええ"
|
| 751 |
+
)
|
| 752 |
+
assert repr(df) == expected
|
| 753 |
+
|
| 754 |
+
# MultiIndex
|
| 755 |
+
idx = MultiIndex.from_tuples(
|
| 756 |
+
[("あ", "いい"), ("う", "え"), ("おおお", "かかかか"), ("き", "くく")]
|
| 757 |
+
)
|
| 758 |
+
df = DataFrame(
|
| 759 |
+
{
|
| 760 |
+
"a": ["あああああ", "い", "う", "えええ"],
|
| 761 |
+
"b": ["あ", "いいい", "う", "ええええええ"],
|
| 762 |
+
},
|
| 763 |
+
index=idx,
|
| 764 |
+
)
|
| 765 |
+
expected = (
|
| 766 |
+
" a b\n"
|
| 767 |
+
"あ いい あああああ あ\n"
|
| 768 |
+
"う え い いいい\n"
|
| 769 |
+
"おおお かかかか う う\n"
|
| 770 |
+
"き くく えええ ええええええ"
|
| 771 |
+
)
|
| 772 |
+
assert repr(df) == expected
|
| 773 |
+
|
| 774 |
+
# truncate
|
| 775 |
+
with option_context("display.max_rows", 3, "display.max_columns", 3):
|
| 776 |
+
df = DataFrame(
|
| 777 |
+
{
|
| 778 |
+
"a": ["あああああ", "い", "う", "えええ"],
|
| 779 |
+
"b": ["あ", "いいい", "う", "ええええええ"],
|
| 780 |
+
"c": ["お", "か", "ききき", "くくくくくく"],
|
| 781 |
+
"ああああ": ["さ", "し", "す", "せ"],
|
| 782 |
+
},
|
| 783 |
+
columns=["a", "b", "c", "ああああ"],
|
| 784 |
+
)
|
| 785 |
+
|
| 786 |
+
expected = (
|
| 787 |
+
" a ... ああああ\n"
|
| 788 |
+
"0 あああああ ... さ\n"
|
| 789 |
+
".. ... ... ...\n"
|
| 790 |
+
"3 えええ ... せ\n"
|
| 791 |
+
"\n[4 rows x 4 columns]"
|
| 792 |
+
)
|
| 793 |
+
assert repr(df) == expected
|
| 794 |
+
|
| 795 |
+
df.index = ["あああ", "いいいい", "う", "aaa"]
|
| 796 |
+
expected = (
|
| 797 |
+
" a ... ああああ\n"
|
| 798 |
+
"あああ あああああ ... さ\n"
|
| 799 |
+
"... ... ... ...\n"
|
| 800 |
+
"aaa えええ ... せ\n"
|
| 801 |
+
"\n[4 rows x 4 columns]"
|
| 802 |
+
)
|
| 803 |
+
assert repr(df) == expected
|
| 804 |
+
|
| 805 |
+
# ambiguous unicode
|
| 806 |
+
df = DataFrame(
|
| 807 |
+
{
|
| 808 |
+
"b": ["あ", "いいい", "¡¡", "ええええええ"],
|
| 809 |
+
"あああああ": [1, 222, 33333, 4],
|
| 810 |
+
},
|
| 811 |
+
index=["a", "bb", "c", "¡¡¡"],
|
| 812 |
+
)
|
| 813 |
+
expected = (
|
| 814 |
+
" b あああああ\n"
|
| 815 |
+
"a あ 1\n"
|
| 816 |
+
"bb いいい 222\n"
|
| 817 |
+
"c ¡¡ 33333\n"
|
| 818 |
+
"¡¡¡ ええええええ 4"
|
| 819 |
+
)
|
| 820 |
+
assert repr(df) == expected
|
| 821 |
+
|
| 822 |
+
def test_to_string_buffer_all_unicode(self):
|
| 823 |
+
buf = StringIO()
|
| 824 |
+
|
| 825 |
+
empty = DataFrame({"c/\u03c3": Series(dtype=object)})
|
| 826 |
+
nonempty = DataFrame({"c/\u03c3": Series([1, 2, 3])})
|
| 827 |
+
|
| 828 |
+
print(empty, file=buf)
|
| 829 |
+
print(nonempty, file=buf)
|
| 830 |
+
|
| 831 |
+
# this should work
|
| 832 |
+
buf.getvalue()
|
| 833 |
+
|
| 834 |
+
@pytest.mark.parametrize(
|
| 835 |
+
"index_scalar",
|
| 836 |
+
[
|
| 837 |
+
"a" * 10,
|
| 838 |
+
1,
|
| 839 |
+
Timestamp(2020, 1, 1),
|
| 840 |
+
pd.Period("2020-01-01"),
|
| 841 |
+
],
|
| 842 |
+
)
|
| 843 |
+
@pytest.mark.parametrize("h", [10, 20])
|
| 844 |
+
@pytest.mark.parametrize("w", [10, 20])
|
| 845 |
+
def test_to_string_truncate_indices(self, index_scalar, h, w):
|
| 846 |
+
with option_context("display.expand_frame_repr", False):
|
| 847 |
+
df = DataFrame(
|
| 848 |
+
index=[index_scalar] * h, columns=[str(i) * 10 for i in range(w)]
|
| 849 |
+
)
|
| 850 |
+
with option_context("display.max_rows", 15):
|
| 851 |
+
if h == 20:
|
| 852 |
+
assert has_vertically_truncated_repr(df)
|
| 853 |
+
else:
|
| 854 |
+
assert not has_vertically_truncated_repr(df)
|
| 855 |
+
with option_context("display.max_columns", 15):
|
| 856 |
+
if w == 20:
|
| 857 |
+
assert has_horizontally_truncated_repr(df)
|
| 858 |
+
else:
|
| 859 |
+
assert not has_horizontally_truncated_repr(df)
|
| 860 |
+
with option_context("display.max_rows", 15, "display.max_columns", 15):
|
| 861 |
+
if h == 20 and w == 20:
|
| 862 |
+
assert has_doubly_truncated_repr(df)
|
| 863 |
+
else:
|
| 864 |
+
assert not has_doubly_truncated_repr(df)
|
| 865 |
+
|
| 866 |
+
def test_to_string_truncate_multilevel(self):
|
| 867 |
+
arrays = [
|
| 868 |
+
["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"],
|
| 869 |
+
["one", "two", "one", "two", "one", "two", "one", "two"],
|
| 870 |
+
]
|
| 871 |
+
df = DataFrame(index=arrays, columns=arrays)
|
| 872 |
+
with option_context("display.max_rows", 7, "display.max_columns", 7):
|
| 873 |
+
assert has_doubly_truncated_repr(df)
|
| 874 |
+
|
| 875 |
+
@pytest.mark.parametrize("dtype", ["object", "datetime64[us]"])
|
| 876 |
+
def test_truncate_with_different_dtypes(self, dtype):
|
| 877 |
+
# 11594, 12045
|
| 878 |
+
# when truncated the dtypes of the splits can differ
|
| 879 |
+
|
| 880 |
+
# 11594
|
| 881 |
+
ser = Series(
|
| 882 |
+
[datetime(2012, 1, 1)] * 10
|
| 883 |
+
+ [datetime(1012, 1, 2)]
|
| 884 |
+
+ [datetime(2012, 1, 3)] * 10,
|
| 885 |
+
dtype=dtype,
|
| 886 |
+
)
|
| 887 |
+
|
| 888 |
+
with option_context("display.max_rows", 8):
|
| 889 |
+
result = str(ser)
|
| 890 |
+
assert dtype in result
|
| 891 |
+
|
| 892 |
+
def test_truncate_with_different_dtypes2(self):
|
| 893 |
+
# 12045
|
| 894 |
+
df = DataFrame({"text": ["some words"] + [None] * 9}, dtype=object)
|
| 895 |
+
|
| 896 |
+
with option_context("display.max_rows", 8, "display.max_columns", 3):
|
| 897 |
+
result = str(df)
|
| 898 |
+
assert "None" in result
|
| 899 |
+
assert "NaN" not in result
|
| 900 |
+
|
| 901 |
+
def test_truncate_with_different_dtypes_multiindex(self):
|
| 902 |
+
# GH#13000
|
| 903 |
+
df = DataFrame({"Vals": range(100)})
|
| 904 |
+
frame = pd.concat([df], keys=["Sweep"], names=["Sweep", "Index"])
|
| 905 |
+
result = repr(frame)
|
| 906 |
+
|
| 907 |
+
result2 = repr(frame.iloc[:5])
|
| 908 |
+
assert result.startswith(result2)
|
| 909 |
+
|
| 910 |
+
def test_datetimelike_frame(self):
|
| 911 |
+
# GH 12211
|
| 912 |
+
df = DataFrame({"date": [Timestamp("20130101").tz_localize("UTC")] + [NaT] * 5})
|
| 913 |
+
|
| 914 |
+
with option_context("display.max_rows", 5):
|
| 915 |
+
result = str(df)
|
| 916 |
+
assert "2013-01-01 00:00:00+00:00" in result
|
| 917 |
+
assert "NaT" in result
|
| 918 |
+
assert "..." in result
|
| 919 |
+
assert "[6 rows x 1 columns]" in result
|
| 920 |
+
|
| 921 |
+
dts = [Timestamp("2011-01-01", tz="US/Eastern")] * 5 + [NaT] * 5
|
| 922 |
+
df = DataFrame({"dt": dts, "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]})
|
| 923 |
+
with option_context("display.max_rows", 5):
|
| 924 |
+
expected = (
|
| 925 |
+
" dt x\n"
|
| 926 |
+
"0 2011-01-01 00:00:00-05:00 1\n"
|
| 927 |
+
"1 2011-01-01 00:00:00-05:00 2\n"
|
| 928 |
+
".. ... ..\n"
|
| 929 |
+
"8 NaT 9\n"
|
| 930 |
+
"9 NaT 10\n\n"
|
| 931 |
+
"[10 rows x 2 columns]"
|
| 932 |
+
)
|
| 933 |
+
assert repr(df) == expected
|
| 934 |
+
|
| 935 |
+
dts = [NaT] * 5 + [Timestamp("2011-01-01", tz="US/Eastern")] * 5
|
| 936 |
+
df = DataFrame({"dt": dts, "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]})
|
| 937 |
+
with option_context("display.max_rows", 5):
|
| 938 |
+
expected = (
|
| 939 |
+
" dt x\n"
|
| 940 |
+
"0 NaT 1\n"
|
| 941 |
+
"1 NaT 2\n"
|
| 942 |
+
".. ... ..\n"
|
| 943 |
+
"8 2011-01-01 00:00:00-05:00 9\n"
|
| 944 |
+
"9 2011-01-01 00:00:00-05:00 10\n\n"
|
| 945 |
+
"[10 rows x 2 columns]"
|
| 946 |
+
)
|
| 947 |
+
assert repr(df) == expected
|
| 948 |
+
|
| 949 |
+
dts = [Timestamp("2011-01-01", tz="Asia/Tokyo")] * 5 + [
|
| 950 |
+
Timestamp("2011-01-01", tz="US/Eastern")
|
| 951 |
+
] * 5
|
| 952 |
+
df = DataFrame({"dt": dts, "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]})
|
| 953 |
+
with option_context("display.max_rows", 5):
|
| 954 |
+
expected = (
|
| 955 |
+
" dt x\n"
|
| 956 |
+
"0 2011-01-01 00:00:00+09:00 1\n"
|
| 957 |
+
"1 2011-01-01 00:00:00+09:00 2\n"
|
| 958 |
+
".. ... ..\n"
|
| 959 |
+
"8 2011-01-01 00:00:00-05:00 9\n"
|
| 960 |
+
"9 2011-01-01 00:00:00-05:00 10\n\n"
|
| 961 |
+
"[10 rows x 2 columns]"
|
| 962 |
+
)
|
| 963 |
+
assert repr(df) == expected
|
| 964 |
+
|
| 965 |
+
@pytest.mark.parametrize(
|
| 966 |
+
"start_date",
|
| 967 |
+
[
|
| 968 |
+
"2017-01-01 23:59:59.999999999",
|
| 969 |
+
"2017-01-01 23:59:59.99999999",
|
| 970 |
+
"2017-01-01 23:59:59.9999999",
|
| 971 |
+
"2017-01-01 23:59:59.999999",
|
| 972 |
+
"2017-01-01 23:59:59.99999",
|
| 973 |
+
"2017-01-01 23:59:59.9999",
|
| 974 |
+
],
|
| 975 |
+
)
|
| 976 |
+
def test_datetimeindex_highprecision(self, start_date):
|
| 977 |
+
# GH19030
|
| 978 |
+
# Check that high-precision time values for the end of day are
|
| 979 |
+
# included in repr for DatetimeIndex
|
| 980 |
+
df = DataFrame({"A": date_range(start=start_date, freq="D", periods=5)})
|
| 981 |
+
result = str(df)
|
| 982 |
+
assert start_date in result
|
| 983 |
+
|
| 984 |
+
dti = date_range(start=start_date, freq="D", periods=5)
|
| 985 |
+
df = DataFrame({"A": range(5)}, index=dti)
|
| 986 |
+
result = str(df.index)
|
| 987 |
+
assert start_date in result
|
| 988 |
+
|
| 989 |
+
def test_string_repr_encoding(self, datapath):
|
| 990 |
+
filepath = datapath("io", "parser", "data", "unicode_series.csv")
|
| 991 |
+
df = read_csv(filepath, header=None, encoding="latin1")
|
| 992 |
+
repr(df)
|
| 993 |
+
repr(df[1])
|
| 994 |
+
|
| 995 |
+
def test_repr_corner(self):
|
| 996 |
+
# representing infs poses no problems
|
| 997 |
+
df = DataFrame({"foo": [-np.inf, np.inf]})
|
| 998 |
+
repr(df)
|
| 999 |
+
|
| 1000 |
+
def test_frame_info_encoding(self):
|
| 1001 |
+
index = ["'Til There Was You (1997)", "ldum klaka (Cold Fever) (1994)"]
|
| 1002 |
+
with option_context("display.max_rows", 1):
|
| 1003 |
+
df = DataFrame(columns=["a", "b", "c"], index=index)
|
| 1004 |
+
repr(df)
|
| 1005 |
+
repr(df.T)
|
| 1006 |
+
|
| 1007 |
+
def test_wide_repr(self):
|
| 1008 |
+
with option_context(
|
| 1009 |
+
"mode.sim_interactive",
|
| 1010 |
+
True,
|
| 1011 |
+
"display.show_dimensions",
|
| 1012 |
+
True,
|
| 1013 |
+
"display.max_columns",
|
| 1014 |
+
20,
|
| 1015 |
+
):
|
| 1016 |
+
max_cols = get_option("display.max_columns")
|
| 1017 |
+
df = DataFrame([["a" * 25] * (max_cols - 1)] * 10)
|
| 1018 |
+
with option_context("display.expand_frame_repr", False):
|
| 1019 |
+
rep_str = repr(df)
|
| 1020 |
+
|
| 1021 |
+
assert f"10 rows x {max_cols - 1} columns" in rep_str
|
| 1022 |
+
with option_context("display.expand_frame_repr", True):
|
| 1023 |
+
wide_repr = repr(df)
|
| 1024 |
+
assert rep_str != wide_repr
|
| 1025 |
+
|
| 1026 |
+
with option_context("display.width", 120):
|
| 1027 |
+
wider_repr = repr(df)
|
| 1028 |
+
assert len(wider_repr) < len(wide_repr)
|
| 1029 |
+
|
| 1030 |
+
def test_wide_repr_wide_columns(self):
|
| 1031 |
+
with option_context("mode.sim_interactive", True, "display.max_columns", 20):
|
| 1032 |
+
df = DataFrame(
|
| 1033 |
+
np.random.default_rng(2).standard_normal((5, 3)),
|
| 1034 |
+
columns=["a" * 90, "b" * 90, "c" * 90],
|
| 1035 |
+
)
|
| 1036 |
+
rep_str = repr(df)
|
| 1037 |
+
|
| 1038 |
+
assert len(rep_str.splitlines()) == 20
|
| 1039 |
+
|
| 1040 |
+
def test_wide_repr_named(self):
|
| 1041 |
+
with option_context("mode.sim_interactive", True, "display.max_columns", 20):
|
| 1042 |
+
max_cols = get_option("display.max_columns")
|
| 1043 |
+
df = DataFrame([["a" * 25] * (max_cols - 1)] * 10)
|
| 1044 |
+
df.index.name = "DataFrame Index"
|
| 1045 |
+
with option_context("display.expand_frame_repr", False):
|
| 1046 |
+
rep_str = repr(df)
|
| 1047 |
+
with option_context("display.expand_frame_repr", True):
|
| 1048 |
+
wide_repr = repr(df)
|
| 1049 |
+
assert rep_str != wide_repr
|
| 1050 |
+
|
| 1051 |
+
with option_context("display.width", 150):
|
| 1052 |
+
wider_repr = repr(df)
|
| 1053 |
+
assert len(wider_repr) < len(wide_repr)
|
| 1054 |
+
|
| 1055 |
+
for line in wide_repr.splitlines()[1::13]:
|
| 1056 |
+
assert "DataFrame Index" in line
|
| 1057 |
+
|
| 1058 |
+
def test_wide_repr_multiindex(self):
|
| 1059 |
+
with option_context("mode.sim_interactive", True, "display.max_columns", 20):
|
| 1060 |
+
midx = MultiIndex.from_arrays([["a" * 5] * 10] * 2)
|
| 1061 |
+
max_cols = get_option("display.max_columns")
|
| 1062 |
+
df = DataFrame([["a" * 25] * (max_cols - 1)] * 10, index=midx)
|
| 1063 |
+
df.index.names = ["Level 0", "Level 1"]
|
| 1064 |
+
with option_context("display.expand_frame_repr", False):
|
| 1065 |
+
rep_str = repr(df)
|
| 1066 |
+
with option_context("display.expand_frame_repr", True):
|
| 1067 |
+
wide_repr = repr(df)
|
| 1068 |
+
assert rep_str != wide_repr
|
| 1069 |
+
|
| 1070 |
+
with option_context("display.width", 150):
|
| 1071 |
+
wider_repr = repr(df)
|
| 1072 |
+
assert len(wider_repr) < len(wide_repr)
|
| 1073 |
+
|
| 1074 |
+
for line in wide_repr.splitlines()[1::13]:
|
| 1075 |
+
assert "Level 0 Level 1" in line
|
| 1076 |
+
|
| 1077 |
+
def test_wide_repr_multiindex_cols(self):
|
| 1078 |
+
with option_context("mode.sim_interactive", True, "display.max_columns", 20):
|
| 1079 |
+
max_cols = get_option("display.max_columns")
|
| 1080 |
+
midx = MultiIndex.from_arrays([["a" * 5] * 10] * 2)
|
| 1081 |
+
mcols = MultiIndex.from_arrays([["b" * 3] * (max_cols - 1)] * 2)
|
| 1082 |
+
df = DataFrame(
|
| 1083 |
+
[["c" * 25] * (max_cols - 1)] * 10, index=midx, columns=mcols
|
| 1084 |
+
)
|
| 1085 |
+
df.index.names = ["Level 0", "Level 1"]
|
| 1086 |
+
with option_context("display.expand_frame_repr", False):
|
| 1087 |
+
rep_str = repr(df)
|
| 1088 |
+
with option_context("display.expand_frame_repr", True):
|
| 1089 |
+
wide_repr = repr(df)
|
| 1090 |
+
assert rep_str != wide_repr
|
| 1091 |
+
|
| 1092 |
+
with option_context("display.width", 150, "display.max_columns", 20):
|
| 1093 |
+
wider_repr = repr(df)
|
| 1094 |
+
assert len(wider_repr) < len(wide_repr)
|
| 1095 |
+
|
| 1096 |
+
def test_wide_repr_unicode(self):
|
| 1097 |
+
with option_context("mode.sim_interactive", True, "display.max_columns", 20):
|
| 1098 |
+
max_cols = 20
|
| 1099 |
+
df = DataFrame([["a" * 25] * 10] * (max_cols - 1))
|
| 1100 |
+
with option_context("display.expand_frame_repr", False):
|
| 1101 |
+
rep_str = repr(df)
|
| 1102 |
+
with option_context("display.expand_frame_repr", True):
|
| 1103 |
+
wide_repr = repr(df)
|
| 1104 |
+
assert rep_str != wide_repr
|
| 1105 |
+
|
| 1106 |
+
with option_context("display.width", 150):
|
| 1107 |
+
wider_repr = repr(df)
|
| 1108 |
+
assert len(wider_repr) < len(wide_repr)
|
| 1109 |
+
|
| 1110 |
+
def test_wide_repr_wide_long_columns(self):
|
| 1111 |
+
with option_context("mode.sim_interactive", True):
|
| 1112 |
+
df = DataFrame({"a": ["a" * 30, "b" * 30], "b": ["c" * 70, "d" * 80]})
|
| 1113 |
+
|
| 1114 |
+
result = repr(df)
|
| 1115 |
+
assert "ccccc" in result
|
| 1116 |
+
assert "ddddd" in result
|
| 1117 |
+
|
| 1118 |
+
def test_long_series(self):
|
| 1119 |
+
n = 1000
|
| 1120 |
+
s = Series(
|
| 1121 |
+
np.random.default_rng(2).integers(-50, 50, n),
|
| 1122 |
+
index=[f"s{x:04d}" for x in range(n)],
|
| 1123 |
+
dtype="int64",
|
| 1124 |
+
)
|
| 1125 |
+
|
| 1126 |
+
str_rep = str(s)
|
| 1127 |
+
nmatches = len(re.findall("dtype", str_rep))
|
| 1128 |
+
assert nmatches == 1
|
| 1129 |
+
|
| 1130 |
+
def test_to_string_ascii_error(self):
|
| 1131 |
+
data = [
|
| 1132 |
+
(
|
| 1133 |
+
"0 ",
|
| 1134 |
+
" .gitignore ",
|
| 1135 |
+
" 5 ",
|
| 1136 |
+
" \xe2\x80\xa2\xe2\x80\xa2\xe2\x80\xa2\xe2\x80\xa2\xe2\x80\xa2",
|
| 1137 |
+
)
|
| 1138 |
+
]
|
| 1139 |
+
df = DataFrame(data)
|
| 1140 |
+
|
| 1141 |
+
# it works!
|
| 1142 |
+
repr(df)
|
| 1143 |
+
|
| 1144 |
+
def test_show_dimensions(self):
|
| 1145 |
+
df = DataFrame(123, index=range(10, 15), columns=range(30))
|
| 1146 |
+
|
| 1147 |
+
with option_context(
|
| 1148 |
+
"display.max_rows",
|
| 1149 |
+
10,
|
| 1150 |
+
"display.max_columns",
|
| 1151 |
+
40,
|
| 1152 |
+
"display.width",
|
| 1153 |
+
500,
|
| 1154 |
+
"display.expand_frame_repr",
|
| 1155 |
+
"info",
|
| 1156 |
+
"display.show_dimensions",
|
| 1157 |
+
True,
|
| 1158 |
+
):
|
| 1159 |
+
assert "5 rows" in str(df)
|
| 1160 |
+
assert "5 rows" in df._repr_html_()
|
| 1161 |
+
with option_context(
|
| 1162 |
+
"display.max_rows",
|
| 1163 |
+
10,
|
| 1164 |
+
"display.max_columns",
|
| 1165 |
+
40,
|
| 1166 |
+
"display.width",
|
| 1167 |
+
500,
|
| 1168 |
+
"display.expand_frame_repr",
|
| 1169 |
+
"info",
|
| 1170 |
+
"display.show_dimensions",
|
| 1171 |
+
False,
|
| 1172 |
+
):
|
| 1173 |
+
assert "5 rows" not in str(df)
|
| 1174 |
+
assert "5 rows" not in df._repr_html_()
|
| 1175 |
+
with option_context(
|
| 1176 |
+
"display.max_rows",
|
| 1177 |
+
2,
|
| 1178 |
+
"display.max_columns",
|
| 1179 |
+
2,
|
| 1180 |
+
"display.width",
|
| 1181 |
+
500,
|
| 1182 |
+
"display.expand_frame_repr",
|
| 1183 |
+
"info",
|
| 1184 |
+
"display.show_dimensions",
|
| 1185 |
+
"truncate",
|
| 1186 |
+
):
|
| 1187 |
+
assert "5 rows" in str(df)
|
| 1188 |
+
assert "5 rows" in df._repr_html_()
|
| 1189 |
+
with option_context(
|
| 1190 |
+
"display.max_rows",
|
| 1191 |
+
10,
|
| 1192 |
+
"display.max_columns",
|
| 1193 |
+
40,
|
| 1194 |
+
"display.width",
|
| 1195 |
+
500,
|
| 1196 |
+
"display.expand_frame_repr",
|
| 1197 |
+
"info",
|
| 1198 |
+
"display.show_dimensions",
|
| 1199 |
+
"truncate",
|
| 1200 |
+
):
|
| 1201 |
+
assert "5 rows" not in str(df)
|
| 1202 |
+
assert "5 rows" not in df._repr_html_()
|
| 1203 |
+
|
| 1204 |
+
def test_info_repr(self):
|
| 1205 |
+
# GH#21746 For tests inside a terminal (i.e. not CI) we need to detect
|
| 1206 |
+
# the terminal size to ensure that we try to print something "too big"
|
| 1207 |
+
term_width, term_height = get_terminal_size()
|
| 1208 |
+
|
| 1209 |
+
max_rows = 60
|
| 1210 |
+
max_cols = 20 + (max(term_width, 80) - 80) // 4
|
| 1211 |
+
# Long
|
| 1212 |
+
h, w = max_rows + 1, max_cols - 1
|
| 1213 |
+
df = DataFrame({k: np.arange(1, 1 + h) for k in np.arange(w)})
|
| 1214 |
+
assert has_vertically_truncated_repr(df)
|
| 1215 |
+
with option_context("display.large_repr", "info"):
|
| 1216 |
+
assert has_info_repr(df)
|
| 1217 |
+
|
| 1218 |
+
# Wide
|
| 1219 |
+
h, w = max_rows - 1, max_cols + 1
|
| 1220 |
+
df = DataFrame({k: np.arange(1, 1 + h) for k in np.arange(w)})
|
| 1221 |
+
assert has_horizontally_truncated_repr(df)
|
| 1222 |
+
with option_context(
|
| 1223 |
+
"display.large_repr", "info", "display.max_columns", max_cols
|
| 1224 |
+
):
|
| 1225 |
+
assert has_info_repr(df)
|
| 1226 |
+
|
| 1227 |
+
def test_info_repr_max_cols(self):
|
| 1228 |
+
# GH #6939
|
| 1229 |
+
df = DataFrame(np.random.default_rng(2).standard_normal((10, 5)))
|
| 1230 |
+
with option_context(
|
| 1231 |
+
"display.large_repr",
|
| 1232 |
+
"info",
|
| 1233 |
+
"display.max_columns",
|
| 1234 |
+
1,
|
| 1235 |
+
"display.max_info_columns",
|
| 1236 |
+
4,
|
| 1237 |
+
):
|
| 1238 |
+
assert has_non_verbose_info_repr(df)
|
| 1239 |
+
|
| 1240 |
+
with option_context(
|
| 1241 |
+
"display.large_repr",
|
| 1242 |
+
"info",
|
| 1243 |
+
"display.max_columns",
|
| 1244 |
+
1,
|
| 1245 |
+
"display.max_info_columns",
|
| 1246 |
+
5,
|
| 1247 |
+
):
|
| 1248 |
+
assert not has_non_verbose_info_repr(df)
|
| 1249 |
+
|
| 1250 |
+
# FIXME: don't leave commented-out
|
| 1251 |
+
# test verbose overrides
|
| 1252 |
+
# set_option('display.max_info_columns', 4) # exceeded
|
| 1253 |
+
|
| 1254 |
+
def test_pprint_pathological_object(self):
|
| 1255 |
+
"""
|
| 1256 |
+
If the test fails, it at least won't hang.
|
| 1257 |
+
"""
|
| 1258 |
+
|
| 1259 |
+
class A:
|
| 1260 |
+
def __getitem__(self, key):
|
| 1261 |
+
return 3 # obviously simplified
|
| 1262 |
+
|
| 1263 |
+
df = DataFrame([A()])
|
| 1264 |
+
repr(df) # just don't die
|
| 1265 |
+
|
| 1266 |
+
def test_float_trim_zeros(self):
|
| 1267 |
+
vals = [
|
| 1268 |
+
2.08430917305e10,
|
| 1269 |
+
3.52205017305e10,
|
| 1270 |
+
2.30674817305e10,
|
| 1271 |
+
2.03954217305e10,
|
| 1272 |
+
5.59897817305e10,
|
| 1273 |
+
]
|
| 1274 |
+
skip = True
|
| 1275 |
+
for line in repr(DataFrame({"A": vals})).split("\n")[:-2]:
|
| 1276 |
+
if line.startswith("dtype:"):
|
| 1277 |
+
continue
|
| 1278 |
+
if _three_digit_exp():
|
| 1279 |
+
assert ("+010" in line) or skip
|
| 1280 |
+
else:
|
| 1281 |
+
assert ("+10" in line) or skip
|
| 1282 |
+
skip = False
|
| 1283 |
+
|
| 1284 |
+
@pytest.mark.parametrize(
|
| 1285 |
+
"data, expected",
|
| 1286 |
+
[
|
| 1287 |
+
(["3.50"], "0 3.50\ndtype: object"),
|
| 1288 |
+
([1.20, "1.00"], "0 1.2\n1 1.00\ndtype: object"),
|
| 1289 |
+
([np.nan], "0 NaN\ndtype: float64"),
|
| 1290 |
+
([None], "0 None\ndtype: object"),
|
| 1291 |
+
(["3.50", np.nan], "0 3.50\n1 NaN\ndtype: object"),
|
| 1292 |
+
([3.50, np.nan], "0 3.5\n1 NaN\ndtype: float64"),
|
| 1293 |
+
([3.50, np.nan, "3.50"], "0 3.5\n1 NaN\n2 3.50\ndtype: object"),
|
| 1294 |
+
([3.50, None, "3.50"], "0 3.5\n1 None\n2 3.50\ndtype: object"),
|
| 1295 |
+
],
|
| 1296 |
+
)
|
| 1297 |
+
def test_repr_str_float_truncation(self, data, expected, using_infer_string):
|
| 1298 |
+
# GH#38708
|
| 1299 |
+
series = Series(data, dtype=object if "3.50" in data else None)
|
| 1300 |
+
result = repr(series)
|
| 1301 |
+
assert result == expected
|
| 1302 |
+
|
| 1303 |
+
@pytest.mark.parametrize(
|
| 1304 |
+
"float_format,expected",
|
| 1305 |
+
[
|
| 1306 |
+
("{:,.0f}".format, "0 1,000\n1 test\ndtype: object"),
|
| 1307 |
+
("{:.4f}".format, "0 1000.0000\n1 test\ndtype: object"),
|
| 1308 |
+
],
|
| 1309 |
+
)
|
| 1310 |
+
def test_repr_float_format_in_object_col(self, float_format, expected):
|
| 1311 |
+
# GH#40024
|
| 1312 |
+
df = Series([1000.0, "test"])
|
| 1313 |
+
with option_context("display.float_format", float_format):
|
| 1314 |
+
result = repr(df)
|
| 1315 |
+
|
| 1316 |
+
assert result == expected
|
| 1317 |
+
|
| 1318 |
+
def test_period(self):
|
| 1319 |
+
# GH 12615
|
| 1320 |
+
df = DataFrame(
|
| 1321 |
+
{
|
| 1322 |
+
"A": pd.period_range("2013-01", periods=4, freq="M"),
|
| 1323 |
+
"B": [
|
| 1324 |
+
pd.Period("2011-01", freq="M"),
|
| 1325 |
+
pd.Period("2011-02-01", freq="D"),
|
| 1326 |
+
pd.Period("2011-03-01 09:00", freq="h"),
|
| 1327 |
+
pd.Period("2011-04", freq="M"),
|
| 1328 |
+
],
|
| 1329 |
+
"C": list("abcd"),
|
| 1330 |
+
}
|
| 1331 |
+
)
|
| 1332 |
+
exp = (
|
| 1333 |
+
" A B C\n"
|
| 1334 |
+
"0 2013-01 2011-01 a\n"
|
| 1335 |
+
"1 2013-02 2011-02-01 b\n"
|
| 1336 |
+
"2 2013-03 2011-03-01 09:00 c\n"
|
| 1337 |
+
"3 2013-04 2011-04 d"
|
| 1338 |
+
)
|
| 1339 |
+
assert str(df) == exp
|
| 1340 |
+
|
| 1341 |
+
@pytest.mark.parametrize(
|
| 1342 |
+
"length, max_rows, min_rows, expected",
|
| 1343 |
+
[
|
| 1344 |
+
(10, 10, 10, 10),
|
| 1345 |
+
(10, 10, None, 10),
|
| 1346 |
+
(10, 8, None, 8),
|
| 1347 |
+
(20, 30, 10, 30), # max_rows > len(frame), hence max_rows
|
| 1348 |
+
(50, 30, 10, 10), # max_rows < len(frame), hence min_rows
|
| 1349 |
+
(100, 60, 10, 10), # same
|
| 1350 |
+
(60, 60, 10, 60), # edge case
|
| 1351 |
+
(61, 60, 10, 10), # edge case
|
| 1352 |
+
],
|
| 1353 |
+
)
|
| 1354 |
+
def test_max_rows_fitted(self, length, min_rows, max_rows, expected):
|
| 1355 |
+
"""Check that display logic is correct.
|
| 1356 |
+
|
| 1357 |
+
GH #37359
|
| 1358 |
+
|
| 1359 |
+
See description here:
|
| 1360 |
+
https://pandas.pydata.org/docs/dev/user_guide/options.html#frequently-used-options
|
| 1361 |
+
"""
|
| 1362 |
+
formatter = fmt.DataFrameFormatter(
|
| 1363 |
+
DataFrame(np.random.default_rng(2).random((length, 3))),
|
| 1364 |
+
max_rows=max_rows,
|
| 1365 |
+
min_rows=min_rows,
|
| 1366 |
+
)
|
| 1367 |
+
result = formatter.max_rows_fitted
|
| 1368 |
+
assert result == expected
|
| 1369 |
+
|
| 1370 |
+
|
| 1371 |
+
def gen_series_formatting():
|
| 1372 |
+
s1 = Series(["a"] * 100)
|
| 1373 |
+
s2 = Series(["ab"] * 100)
|
| 1374 |
+
s3 = Series(["a", "ab", "abc", "abcd", "abcde", "abcdef"])
|
| 1375 |
+
s4 = s3[::-1]
|
| 1376 |
+
test_sers = {"onel": s1, "twol": s2, "asc": s3, "desc": s4}
|
| 1377 |
+
return test_sers
|
| 1378 |
+
|
| 1379 |
+
|
| 1380 |
+
class TestSeriesFormatting:
|
| 1381 |
+
def test_freq_name_separation(self):
|
| 1382 |
+
s = Series(
|
| 1383 |
+
np.random.default_rng(2).standard_normal(10),
|
| 1384 |
+
index=date_range("1/1/2000", periods=10),
|
| 1385 |
+
name=0,
|
| 1386 |
+
)
|
| 1387 |
+
|
| 1388 |
+
result = repr(s)
|
| 1389 |
+
assert "Freq: D, Name: 0" in result
|
| 1390 |
+
|
| 1391 |
+
def test_unicode_name_in_footer(self):
|
| 1392 |
+
s = Series([1, 2], name="\u05e2\u05d1\u05e8\u05d9\u05ea")
|
| 1393 |
+
sf = fmt.SeriesFormatter(s, name="\u05e2\u05d1\u05e8\u05d9\u05ea")
|
| 1394 |
+
sf._get_footer() # should not raise exception
|
| 1395 |
+
|
| 1396 |
+
def test_east_asian_unicode_series(self, using_infer_string):
|
| 1397 |
+
# not aligned properly because of east asian width
|
| 1398 |
+
|
| 1399 |
+
# unicode index
|
| 1400 |
+
s = Series(["a", "bb", "CCC", "D"], index=["あ", "いい", "ううう", "ええええ"])
|
| 1401 |
+
expected = "".join(
|
| 1402 |
+
[
|
| 1403 |
+
"あ a\n",
|
| 1404 |
+
"いい bb\n",
|
| 1405 |
+
"ううう CCC\n",
|
| 1406 |
+
"ええええ D\ndtype: object",
|
| 1407 |
+
]
|
| 1408 |
+
)
|
| 1409 |
+
if using_infer_string:
|
| 1410 |
+
expected = expected.replace("dtype: object", "dtype: str")
|
| 1411 |
+
assert repr(s) == expected
|
| 1412 |
+
|
| 1413 |
+
# unicode values
|
| 1414 |
+
s = Series(["あ", "いい", "ううう", "ええええ"], index=["a", "bb", "c", "ddd"])
|
| 1415 |
+
expected = "".join(
|
| 1416 |
+
[
|
| 1417 |
+
"a あ\n",
|
| 1418 |
+
"bb いい\n",
|
| 1419 |
+
"c ううう\n",
|
| 1420 |
+
"ddd ええええ\n",
|
| 1421 |
+
"dtype: object",
|
| 1422 |
+
]
|
| 1423 |
+
)
|
| 1424 |
+
if using_infer_string:
|
| 1425 |
+
expected = expected.replace("dtype: object", "dtype: str")
|
| 1426 |
+
assert repr(s) == expected
|
| 1427 |
+
|
| 1428 |
+
# both
|
| 1429 |
+
s = Series(
|
| 1430 |
+
["あ", "いい", "ううう", "ええええ"],
|
| 1431 |
+
index=["ああ", "いいいい", "う", "えええ"],
|
| 1432 |
+
)
|
| 1433 |
+
expected = "".join(
|
| 1434 |
+
[
|
| 1435 |
+
"ああ あ\n",
|
| 1436 |
+
"いいいい いい\n",
|
| 1437 |
+
"う ううう\n",
|
| 1438 |
+
"えええ ええええ\n",
|
| 1439 |
+
"dtype: object",
|
| 1440 |
+
]
|
| 1441 |
+
)
|
| 1442 |
+
if using_infer_string:
|
| 1443 |
+
expected = expected.replace("dtype: object", "dtype: str")
|
| 1444 |
+
assert repr(s) == expected
|
| 1445 |
+
|
| 1446 |
+
# unicode footer
|
| 1447 |
+
s = Series(
|
| 1448 |
+
["あ", "いい", "ううう", "ええええ"],
|
| 1449 |
+
index=["ああ", "いいいい", "う", "えええ"],
|
| 1450 |
+
name="おおおおおおお",
|
| 1451 |
+
)
|
| 1452 |
+
expected = (
|
| 1453 |
+
"ああ あ\nいいいい いい\nう ううう\n"
|
| 1454 |
+
"えええ ええええ\nName: おおおおおおお, dtype: object"
|
| 1455 |
+
)
|
| 1456 |
+
if using_infer_string:
|
| 1457 |
+
expected = expected.replace("dtype: object", "dtype: str")
|
| 1458 |
+
assert repr(s) == expected
|
| 1459 |
+
|
| 1460 |
+
# MultiIndex
|
| 1461 |
+
idx = MultiIndex.from_tuples(
|
| 1462 |
+
[("あ", "いい"), ("う", "え"), ("おおお", "かかかか"), ("き", "くく")]
|
| 1463 |
+
)
|
| 1464 |
+
s = Series([1, 22, 3333, 44444], index=idx)
|
| 1465 |
+
expected = (
|
| 1466 |
+
"あ いい 1\n"
|
| 1467 |
+
"う え 22\n"
|
| 1468 |
+
"おおお かかかか 3333\n"
|
| 1469 |
+
"き くく 44444\ndtype: int64"
|
| 1470 |
+
)
|
| 1471 |
+
assert repr(s) == expected
|
| 1472 |
+
|
| 1473 |
+
# object dtype, shorter than unicode repr
|
| 1474 |
+
s = Series([1, 22, 3333, 44444], index=[1, "AB", np.nan, "あああ"])
|
| 1475 |
+
expected = (
|
| 1476 |
+
"1 1\nAB 22\nNaN 3333\nあああ 44444\ndtype: int64"
|
| 1477 |
+
)
|
| 1478 |
+
assert repr(s) == expected
|
| 1479 |
+
|
| 1480 |
+
# object dtype, longer than unicode repr
|
| 1481 |
+
s = Series(
|
| 1482 |
+
[1, 22, 3333, 44444], index=[1, "AB", Timestamp("2011-01-01"), "あああ"]
|
| 1483 |
+
)
|
| 1484 |
+
expected = (
|
| 1485 |
+
"1 1\n"
|
| 1486 |
+
"AB 22\n"
|
| 1487 |
+
"2011-01-01 00:00:00 3333\n"
|
| 1488 |
+
"あああ 44444\ndtype: int64"
|
| 1489 |
+
)
|
| 1490 |
+
assert repr(s) == expected
|
| 1491 |
+
|
| 1492 |
+
# truncate
|
| 1493 |
+
with option_context("display.max_rows", 3):
|
| 1494 |
+
s = Series(["あ", "いい", "ううう", "ええええ"], name="おおおおおおお")
|
| 1495 |
+
|
| 1496 |
+
expected = (
|
| 1497 |
+
"0 あ\n ... \n"
|
| 1498 |
+
"3 ええええ\n"
|
| 1499 |
+
"Name: おおおおおおお, Length: 4, dtype: object"
|
| 1500 |
+
)
|
| 1501 |
+
if using_infer_string:
|
| 1502 |
+
expected = expected.replace("dtype: object", "dtype: str")
|
| 1503 |
+
assert repr(s) == expected
|
| 1504 |
+
|
| 1505 |
+
s.index = ["ああ", "いいいい", "う", "えええ"]
|
| 1506 |
+
expected = (
|
| 1507 |
+
"ああ あ\n ... \n"
|
| 1508 |
+
"えええ ええええ\n"
|
| 1509 |
+
"Name: おおおおおおお, Length: 4, dtype: object"
|
| 1510 |
+
)
|
| 1511 |
+
if using_infer_string:
|
| 1512 |
+
expected = expected.replace("dtype: object", "dtype: str")
|
| 1513 |
+
assert repr(s) == expected
|
| 1514 |
+
|
| 1515 |
+
# Enable Unicode option -----------------------------------------
|
| 1516 |
+
with option_context("display.unicode.east_asian_width", True):
|
| 1517 |
+
# unicode index
|
| 1518 |
+
s = Series(
|
| 1519 |
+
["a", "bb", "CCC", "D"],
|
| 1520 |
+
index=["あ", "いい", "ううう", "ええええ"],
|
| 1521 |
+
)
|
| 1522 |
+
expected = (
|
| 1523 |
+
"あ a\nいい bb\nううう CCC\n"
|
| 1524 |
+
"ええええ D\ndtype: object"
|
| 1525 |
+
)
|
| 1526 |
+
if using_infer_string:
|
| 1527 |
+
expected = expected.replace("dtype: object", "dtype: str")
|
| 1528 |
+
assert repr(s) == expected
|
| 1529 |
+
|
| 1530 |
+
# unicode values
|
| 1531 |
+
s = Series(
|
| 1532 |
+
["あ", "いい", "ううう", "ええええ"],
|
| 1533 |
+
index=["a", "bb", "c", "ddd"],
|
| 1534 |
+
)
|
| 1535 |
+
expected = (
|
| 1536 |
+
"a あ\nbb いい\nc ううう\n"
|
| 1537 |
+
"ddd ええええ\ndtype: object"
|
| 1538 |
+
)
|
| 1539 |
+
if using_infer_string:
|
| 1540 |
+
expected = expected.replace("dtype: object", "dtype: str")
|
| 1541 |
+
assert repr(s) == expected
|
| 1542 |
+
# both
|
| 1543 |
+
s = Series(
|
| 1544 |
+
["あ", "いい", "ううう", "ええええ"],
|
| 1545 |
+
index=["ああ", "いいいい", "う", "えええ"],
|
| 1546 |
+
)
|
| 1547 |
+
expected = (
|
| 1548 |
+
"ああ あ\n"
|
| 1549 |
+
"いいいい いい\n"
|
| 1550 |
+
"う ううう\n"
|
| 1551 |
+
"えええ ええええ\ndtype: object"
|
| 1552 |
+
)
|
| 1553 |
+
if using_infer_string:
|
| 1554 |
+
expected = expected.replace("dtype: object", "dtype: str")
|
| 1555 |
+
assert repr(s) == expected
|
| 1556 |
+
|
| 1557 |
+
# unicode footer
|
| 1558 |
+
s = Series(
|
| 1559 |
+
["あ", "いい", "ううう", "ええええ"],
|
| 1560 |
+
index=["ああ", "いいいい", "う", "えええ"],
|
| 1561 |
+
name="おおおおおおお",
|
| 1562 |
+
)
|
| 1563 |
+
expected = (
|
| 1564 |
+
"ああ あ\n"
|
| 1565 |
+
"いいいい いい\n"
|
| 1566 |
+
"う ううう\n"
|
| 1567 |
+
"えええ ええええ\n"
|
| 1568 |
+
"Name: おおおおおおお, dtype: object"
|
| 1569 |
+
)
|
| 1570 |
+
if using_infer_string:
|
| 1571 |
+
expected = expected.replace("dtype: object", "dtype: str")
|
| 1572 |
+
assert repr(s) == expected
|
| 1573 |
+
|
| 1574 |
+
# MultiIndex
|
| 1575 |
+
idx = MultiIndex.from_tuples(
|
| 1576 |
+
[("あ", "いい"), ("う", "え"), ("おおお", "かかかか"), ("き", "くく")]
|
| 1577 |
+
)
|
| 1578 |
+
s = Series([1, 22, 3333, 44444], index=idx)
|
| 1579 |
+
expected = (
|
| 1580 |
+
"あ いい 1\n"
|
| 1581 |
+
"う え 22\n"
|
| 1582 |
+
"おおお かかかか 3333\n"
|
| 1583 |
+
"き くく 44444\n"
|
| 1584 |
+
"dtype: int64"
|
| 1585 |
+
)
|
| 1586 |
+
assert repr(s) == expected
|
| 1587 |
+
|
| 1588 |
+
# object dtype, shorter than unicode repr
|
| 1589 |
+
s = Series([1, 22, 3333, 44444], index=[1, "AB", np.nan, "あああ"])
|
| 1590 |
+
expected = (
|
| 1591 |
+
"1 1\nAB 22\nNaN 3333\n"
|
| 1592 |
+
"あああ 44444\ndtype: int64"
|
| 1593 |
+
)
|
| 1594 |
+
assert repr(s) == expected
|
| 1595 |
+
|
| 1596 |
+
# object dtype, longer than unicode repr
|
| 1597 |
+
s = Series(
|
| 1598 |
+
[1, 22, 3333, 44444],
|
| 1599 |
+
index=[1, "AB", Timestamp("2011-01-01"), "あああ"],
|
| 1600 |
+
)
|
| 1601 |
+
expected = (
|
| 1602 |
+
"1 1\n"
|
| 1603 |
+
"AB 22\n"
|
| 1604 |
+
"2011-01-01 00:00:00 3333\n"
|
| 1605 |
+
"あああ 44444\ndtype: int64"
|
| 1606 |
+
)
|
| 1607 |
+
assert repr(s) == expected
|
| 1608 |
+
|
| 1609 |
+
# truncate
|
| 1610 |
+
with option_context("display.max_rows", 3):
|
| 1611 |
+
s = Series(["あ", "いい", "ううう", "ええええ"], name="おおおおおおお")
|
| 1612 |
+
expected = (
|
| 1613 |
+
"0 あ\n ... \n"
|
| 1614 |
+
"3 ええええ\n"
|
| 1615 |
+
"Name: おおおおおおお, Length: 4, dtype: object"
|
| 1616 |
+
)
|
| 1617 |
+
if using_infer_string:
|
| 1618 |
+
expected = expected.replace("dtype: object", "dtype: str")
|
| 1619 |
+
assert repr(s) == expected
|
| 1620 |
+
|
| 1621 |
+
s.index = ["ああ", "いいいい", "う", "えええ"]
|
| 1622 |
+
expected = (
|
| 1623 |
+
"ああ あ\n"
|
| 1624 |
+
" ... \n"
|
| 1625 |
+
"えええ ええええ\n"
|
| 1626 |
+
"Name: おおおおおおお, Length: 4, dtype: object"
|
| 1627 |
+
)
|
| 1628 |
+
if using_infer_string:
|
| 1629 |
+
expected = expected.replace("dtype: object", "dtype: str")
|
| 1630 |
+
assert repr(s) == expected
|
| 1631 |
+
|
| 1632 |
+
# ambiguous unicode
|
| 1633 |
+
s = Series(
|
| 1634 |
+
["¡¡", "い¡¡", "ううう", "ええええ"],
|
| 1635 |
+
index=["ああ", "¡¡¡¡いい", "¡¡", "えええ"],
|
| 1636 |
+
)
|
| 1637 |
+
expected = (
|
| 1638 |
+
"ああ ¡¡\n"
|
| 1639 |
+
"¡¡¡¡いい い¡¡\n"
|
| 1640 |
+
"¡¡ ううう\n"
|
| 1641 |
+
"えええ ええええ\ndtype: object"
|
| 1642 |
+
)
|
| 1643 |
+
if using_infer_string:
|
| 1644 |
+
expected = expected.replace("dtype: object", "dtype: str")
|
| 1645 |
+
assert repr(s) == expected
|
| 1646 |
+
|
| 1647 |
+
def test_float_trim_zeros(self):
|
| 1648 |
+
vals = [
|
| 1649 |
+
2.08430917305e10,
|
| 1650 |
+
3.52205017305e10,
|
| 1651 |
+
2.30674817305e10,
|
| 1652 |
+
2.03954217305e10,
|
| 1653 |
+
5.59897817305e10,
|
| 1654 |
+
]
|
| 1655 |
+
for line in repr(Series(vals)).split("\n"):
|
| 1656 |
+
if line.startswith("dtype:"):
|
| 1657 |
+
continue
|
| 1658 |
+
if _three_digit_exp():
|
| 1659 |
+
assert "+010" in line
|
| 1660 |
+
else:
|
| 1661 |
+
assert "+10" in line
|
| 1662 |
+
|
| 1663 |
+
@pytest.mark.parametrize(
|
| 1664 |
+
"start_date",
|
| 1665 |
+
[
|
| 1666 |
+
"2017-01-01 23:59:59.999999999",
|
| 1667 |
+
"2017-01-01 23:59:59.99999999",
|
| 1668 |
+
"2017-01-01 23:59:59.9999999",
|
| 1669 |
+
"2017-01-01 23:59:59.999999",
|
| 1670 |
+
"2017-01-01 23:59:59.99999",
|
| 1671 |
+
"2017-01-01 23:59:59.9999",
|
| 1672 |
+
],
|
| 1673 |
+
)
|
| 1674 |
+
def test_datetimeindex_highprecision(self, start_date):
|
| 1675 |
+
# GH19030
|
| 1676 |
+
# Check that high-precision time values for the end of day are
|
| 1677 |
+
# included in repr for DatetimeIndex
|
| 1678 |
+
s1 = Series(date_range(start=start_date, freq="D", periods=5))
|
| 1679 |
+
result = str(s1)
|
| 1680 |
+
assert start_date in result
|
| 1681 |
+
|
| 1682 |
+
dti = date_range(start=start_date, freq="D", periods=5)
|
| 1683 |
+
s2 = Series(3, index=dti)
|
| 1684 |
+
result = str(s2.index)
|
| 1685 |
+
assert start_date in result
|
| 1686 |
+
|
| 1687 |
+
def test_mixed_datetime64(self):
|
| 1688 |
+
df = DataFrame({"A": [1, 2], "B": ["2012-01-01", "2012-01-02"]})
|
| 1689 |
+
df["B"] = pd.to_datetime(df.B)
|
| 1690 |
+
|
| 1691 |
+
result = repr(df.loc[0])
|
| 1692 |
+
assert "2012-01-01" in result
|
| 1693 |
+
|
| 1694 |
+
def test_period(self):
|
| 1695 |
+
# GH 12615
|
| 1696 |
+
index = pd.period_range("2013-01", periods=6, freq="M")
|
| 1697 |
+
s = Series(np.arange(6, dtype="int64"), index=index)
|
| 1698 |
+
exp = (
|
| 1699 |
+
"2013-01 0\n"
|
| 1700 |
+
"2013-02 1\n"
|
| 1701 |
+
"2013-03 2\n"
|
| 1702 |
+
"2013-04 3\n"
|
| 1703 |
+
"2013-05 4\n"
|
| 1704 |
+
"2013-06 5\n"
|
| 1705 |
+
"Freq: M, dtype: int64"
|
| 1706 |
+
)
|
| 1707 |
+
assert str(s) == exp
|
| 1708 |
+
|
| 1709 |
+
s = Series(index)
|
| 1710 |
+
exp = (
|
| 1711 |
+
"0 2013-01\n"
|
| 1712 |
+
"1 2013-02\n"
|
| 1713 |
+
"2 2013-03\n"
|
| 1714 |
+
"3 2013-04\n"
|
| 1715 |
+
"4 2013-05\n"
|
| 1716 |
+
"5 2013-06\n"
|
| 1717 |
+
"dtype: period[M]"
|
| 1718 |
+
)
|
| 1719 |
+
assert str(s) == exp
|
| 1720 |
+
|
| 1721 |
+
# periods with mixed freq
|
| 1722 |
+
s = Series(
|
| 1723 |
+
[
|
| 1724 |
+
pd.Period("2011-01", freq="M"),
|
| 1725 |
+
pd.Period("2011-02-01", freq="D"),
|
| 1726 |
+
pd.Period("2011-03-01 09:00", freq="h"),
|
| 1727 |
+
]
|
| 1728 |
+
)
|
| 1729 |
+
exp = (
|
| 1730 |
+
"0 2011-01\n1 2011-02-01\n"
|
| 1731 |
+
"2 2011-03-01 09:00\ndtype: object"
|
| 1732 |
+
)
|
| 1733 |
+
assert str(s) == exp
|
| 1734 |
+
|
| 1735 |
+
def test_max_multi_index_display(self):
|
| 1736 |
+
# GH 7101
|
| 1737 |
+
|
| 1738 |
+
# doc example (indexing.rst)
|
| 1739 |
+
|
| 1740 |
+
# multi-index
|
| 1741 |
+
arrays = [
|
| 1742 |
+
["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"],
|
| 1743 |
+
["one", "two", "one", "two", "one", "two", "one", "two"],
|
| 1744 |
+
]
|
| 1745 |
+
tuples = list(zip(*arrays))
|
| 1746 |
+
index = MultiIndex.from_tuples(tuples, names=["first", "second"])
|
| 1747 |
+
s = Series(np.random.default_rng(2).standard_normal(8), index=index)
|
| 1748 |
+
|
| 1749 |
+
with option_context("display.max_rows", 10):
|
| 1750 |
+
assert len(str(s).split("\n")) == 10
|
| 1751 |
+
with option_context("display.max_rows", 3):
|
| 1752 |
+
assert len(str(s).split("\n")) == 5
|
| 1753 |
+
with option_context("display.max_rows", 2):
|
| 1754 |
+
assert len(str(s).split("\n")) == 5
|
| 1755 |
+
with option_context("display.max_rows", 1):
|
| 1756 |
+
assert len(str(s).split("\n")) == 4
|
| 1757 |
+
with option_context("display.max_rows", 0):
|
| 1758 |
+
assert len(str(s).split("\n")) == 10
|
| 1759 |
+
|
| 1760 |
+
# index
|
| 1761 |
+
s = Series(np.random.default_rng(2).standard_normal(8), None)
|
| 1762 |
+
|
| 1763 |
+
with option_context("display.max_rows", 10):
|
| 1764 |
+
assert len(str(s).split("\n")) == 9
|
| 1765 |
+
with option_context("display.max_rows", 3):
|
| 1766 |
+
assert len(str(s).split("\n")) == 4
|
| 1767 |
+
with option_context("display.max_rows", 2):
|
| 1768 |
+
assert len(str(s).split("\n")) == 4
|
| 1769 |
+
with option_context("display.max_rows", 1):
|
| 1770 |
+
assert len(str(s).split("\n")) == 3
|
| 1771 |
+
with option_context("display.max_rows", 0):
|
| 1772 |
+
assert len(str(s).split("\n")) == 9
|
| 1773 |
+
|
| 1774 |
+
# Make sure #8532 is fixed
|
| 1775 |
+
def test_consistent_format(self):
|
| 1776 |
+
s = Series([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0.9999, 1, 1] * 10)
|
| 1777 |
+
with option_context("display.max_rows", 10, "display.show_dimensions", False):
|
| 1778 |
+
res = repr(s)
|
| 1779 |
+
exp = (
|
| 1780 |
+
"0 1.0000\n1 1.0000\n2 1.0000\n3 "
|
| 1781 |
+
"1.0000\n4 1.0000\n ... \n125 "
|
| 1782 |
+
"1.0000\n126 1.0000\n127 0.9999\n128 "
|
| 1783 |
+
"1.0000\n129 1.0000\ndtype: float64"
|
| 1784 |
+
)
|
| 1785 |
+
assert res == exp
|
| 1786 |
+
|
| 1787 |
+
def chck_ncols(self, s):
|
| 1788 |
+
lines = [
|
| 1789 |
+
line for line in repr(s).split("\n") if not re.match(r"[^\.]*\.+", line)
|
| 1790 |
+
][:-1]
|
| 1791 |
+
ncolsizes = len({len(line.strip()) for line in lines})
|
| 1792 |
+
assert ncolsizes == 1
|
| 1793 |
+
|
| 1794 |
+
def test_format_explicit(self, using_infer_string):
|
| 1795 |
+
test_sers = gen_series_formatting()
|
| 1796 |
+
with option_context("display.max_rows", 4, "display.show_dimensions", False):
|
| 1797 |
+
res = repr(test_sers["onel"])
|
| 1798 |
+
exp = "0 a\n1 a\n ..\n98 a\n99 a\ndtype: object"
|
| 1799 |
+
if using_infer_string:
|
| 1800 |
+
exp = exp.replace("dtype: object", "dtype: str")
|
| 1801 |
+
assert exp == res
|
| 1802 |
+
res = repr(test_sers["twol"])
|
| 1803 |
+
exp = "0 ab\n1 ab\n ..\n98 ab\n99 ab\ndtype: object"
|
| 1804 |
+
if using_infer_string:
|
| 1805 |
+
exp = exp.replace("dtype: object", "dtype: str")
|
| 1806 |
+
assert exp == res
|
| 1807 |
+
res = repr(test_sers["asc"])
|
| 1808 |
+
exp = (
|
| 1809 |
+
"0 a\n1 ab\n ... \n4 abcde\n5 "
|
| 1810 |
+
"abcdef\ndtype: object"
|
| 1811 |
+
)
|
| 1812 |
+
if using_infer_string:
|
| 1813 |
+
exp = exp.replace("dtype: object", "dtype: str")
|
| 1814 |
+
assert exp == res
|
| 1815 |
+
res = repr(test_sers["desc"])
|
| 1816 |
+
exp = (
|
| 1817 |
+
"5 abcdef\n4 abcde\n ... \n1 ab\n0 "
|
| 1818 |
+
"a\ndtype: object"
|
| 1819 |
+
)
|
| 1820 |
+
if using_infer_string:
|
| 1821 |
+
exp = exp.replace("dtype: object", "dtype: str")
|
| 1822 |
+
assert exp == res
|
| 1823 |
+
|
| 1824 |
+
def test_ncols(self):
|
| 1825 |
+
test_sers = gen_series_formatting()
|
| 1826 |
+
for s in test_sers.values():
|
| 1827 |
+
self.chck_ncols(s)
|
| 1828 |
+
|
| 1829 |
+
def test_max_rows_eq_one(self):
|
| 1830 |
+
s = Series(range(10), dtype="int64")
|
| 1831 |
+
with option_context("display.max_rows", 1):
|
| 1832 |
+
strrepr = repr(s).split("\n")
|
| 1833 |
+
exp1 = ["0", "0"]
|
| 1834 |
+
res1 = strrepr[0].split()
|
| 1835 |
+
assert exp1 == res1
|
| 1836 |
+
exp2 = [".."]
|
| 1837 |
+
res2 = strrepr[1].split()
|
| 1838 |
+
assert exp2 == res2
|
| 1839 |
+
|
| 1840 |
+
def test_truncate_ndots(self):
|
| 1841 |
+
def getndots(s):
|
| 1842 |
+
return len(re.match(r"[^\.]*(\.*)", s).groups()[0])
|
| 1843 |
+
|
| 1844 |
+
s = Series([0, 2, 3, 6])
|
| 1845 |
+
with option_context("display.max_rows", 2):
|
| 1846 |
+
strrepr = repr(s).replace("\n", "")
|
| 1847 |
+
assert getndots(strrepr) == 2
|
| 1848 |
+
|
| 1849 |
+
s = Series([0, 100, 200, 400])
|
| 1850 |
+
with option_context("display.max_rows", 2):
|
| 1851 |
+
strrepr = repr(s).replace("\n", "")
|
| 1852 |
+
assert getndots(strrepr) == 3
|
| 1853 |
+
|
| 1854 |
+
def test_show_dimensions(self):
|
| 1855 |
+
# gh-7117
|
| 1856 |
+
s = Series(range(5))
|
| 1857 |
+
|
| 1858 |
+
assert "Length" not in repr(s)
|
| 1859 |
+
|
| 1860 |
+
with option_context("display.max_rows", 4):
|
| 1861 |
+
assert "Length" in repr(s)
|
| 1862 |
+
|
| 1863 |
+
with option_context("display.show_dimensions", True):
|
| 1864 |
+
assert "Length" in repr(s)
|
| 1865 |
+
|
| 1866 |
+
with option_context("display.max_rows", 4, "display.show_dimensions", False):
|
| 1867 |
+
assert "Length" not in repr(s)
|
| 1868 |
+
|
| 1869 |
+
def test_repr_min_rows(self):
|
| 1870 |
+
s = Series(range(20))
|
| 1871 |
+
|
| 1872 |
+
# default setting no truncation even if above min_rows
|
| 1873 |
+
assert ".." not in repr(s)
|
| 1874 |
+
|
| 1875 |
+
s = Series(range(61))
|
| 1876 |
+
|
| 1877 |
+
# default of max_rows 60 triggers truncation if above
|
| 1878 |
+
assert ".." in repr(s)
|
| 1879 |
+
|
| 1880 |
+
with option_context("display.max_rows", 10, "display.min_rows", 4):
|
| 1881 |
+
# truncated after first two rows
|
| 1882 |
+
assert ".." in repr(s)
|
| 1883 |
+
assert "2 " not in repr(s)
|
| 1884 |
+
|
| 1885 |
+
with option_context("display.max_rows", 12, "display.min_rows", None):
|
| 1886 |
+
# when set to None, follow value of max_rows
|
| 1887 |
+
assert "5 5" in repr(s)
|
| 1888 |
+
|
| 1889 |
+
with option_context("display.max_rows", 10, "display.min_rows", 12):
|
| 1890 |
+
# when set value higher as max_rows, use the minimum
|
| 1891 |
+
assert "5 5" not in repr(s)
|
| 1892 |
+
|
| 1893 |
+
with option_context("display.max_rows", None, "display.min_rows", 12):
|
| 1894 |
+
# max_rows of None -> never truncate
|
| 1895 |
+
assert ".." not in repr(s)
|
| 1896 |
+
|
| 1897 |
+
|
| 1898 |
+
class TestGenericArrayFormatter:
|
| 1899 |
+
def test_1d_array(self):
|
| 1900 |
+
# _GenericArrayFormatter is used on types for which there isn't a dedicated
|
| 1901 |
+
# formatter. np.bool_ is one of those types.
|
| 1902 |
+
obj = fmt._GenericArrayFormatter(np.array([True, False]))
|
| 1903 |
+
res = obj.get_result()
|
| 1904 |
+
assert len(res) == 2
|
| 1905 |
+
# Results should be right-justified.
|
| 1906 |
+
assert res[0] == " True"
|
| 1907 |
+
assert res[1] == " False"
|
| 1908 |
+
|
| 1909 |
+
def test_2d_array(self):
|
| 1910 |
+
obj = fmt._GenericArrayFormatter(np.array([[True, False], [False, True]]))
|
| 1911 |
+
res = obj.get_result()
|
| 1912 |
+
assert len(res) == 2
|
| 1913 |
+
assert res[0] == " [True, False]"
|
| 1914 |
+
assert res[1] == " [False, True]"
|
| 1915 |
+
|
| 1916 |
+
def test_3d_array(self):
|
| 1917 |
+
obj = fmt._GenericArrayFormatter(
|
| 1918 |
+
np.array([[[True, True], [False, False]], [[False, True], [True, False]]])
|
| 1919 |
+
)
|
| 1920 |
+
res = obj.get_result()
|
| 1921 |
+
assert len(res) == 2
|
| 1922 |
+
assert res[0] == " [[True, True], [False, False]]"
|
| 1923 |
+
assert res[1] == " [[False, True], [True, False]]"
|
| 1924 |
+
|
| 1925 |
+
|
| 1926 |
+
def _three_digit_exp():
|
| 1927 |
+
return f"{1.7e8:.4g}" == "1.7e+008"
|
| 1928 |
+
|
| 1929 |
+
|
| 1930 |
+
class TestFloatArrayFormatter:
|
| 1931 |
+
def test_misc(self):
|
| 1932 |
+
obj = fmt.FloatArrayFormatter(np.array([], dtype=np.float64))
|
| 1933 |
+
result = obj.get_result()
|
| 1934 |
+
assert len(result) == 0
|
| 1935 |
+
|
| 1936 |
+
def test_format(self):
|
| 1937 |
+
obj = fmt.FloatArrayFormatter(np.array([12, 0], dtype=np.float64))
|
| 1938 |
+
result = obj.get_result()
|
| 1939 |
+
assert result[0] == " 12.0"
|
| 1940 |
+
assert result[1] == " 0.0"
|
| 1941 |
+
|
| 1942 |
+
def test_output_display_precision_trailing_zeroes(self):
|
| 1943 |
+
# Issue #20359: trimming zeros while there is no decimal point
|
| 1944 |
+
|
| 1945 |
+
# Happens when display precision is set to zero
|
| 1946 |
+
with option_context("display.precision", 0):
|
| 1947 |
+
s = Series([840.0, 4200.0])
|
| 1948 |
+
expected_output = "0 840\n1 4200\ndtype: float64"
|
| 1949 |
+
assert str(s) == expected_output
|
| 1950 |
+
|
| 1951 |
+
@pytest.mark.parametrize(
|
| 1952 |
+
"value,expected",
|
| 1953 |
+
[
|
| 1954 |
+
([9.4444], " 0\n0 9"),
|
| 1955 |
+
([0.49], " 0\n0 5e-01"),
|
| 1956 |
+
([10.9999], " 0\n0 11"),
|
| 1957 |
+
([9.5444, 9.6], " 0\n0 10\n1 10"),
|
| 1958 |
+
([0.46, 0.78, -9.9999], " 0\n0 5e-01\n1 8e-01\n2 -1e+01"),
|
| 1959 |
+
],
|
| 1960 |
+
)
|
| 1961 |
+
def test_set_option_precision(self, value, expected):
|
| 1962 |
+
# Issue #30122
|
| 1963 |
+
# Precision was incorrectly shown
|
| 1964 |
+
|
| 1965 |
+
with option_context("display.precision", 0):
|
| 1966 |
+
df_value = DataFrame(value)
|
| 1967 |
+
assert str(df_value) == expected
|
| 1968 |
+
|
| 1969 |
+
def test_output_significant_digits(self):
|
| 1970 |
+
# Issue #9764
|
| 1971 |
+
|
| 1972 |
+
# In case default display precision changes:
|
| 1973 |
+
with option_context("display.precision", 6):
|
| 1974 |
+
# DataFrame example from issue #9764
|
| 1975 |
+
d = DataFrame(
|
| 1976 |
+
{
|
| 1977 |
+
"col1": [
|
| 1978 |
+
9.999e-8,
|
| 1979 |
+
1e-7,
|
| 1980 |
+
1.0001e-7,
|
| 1981 |
+
2e-7,
|
| 1982 |
+
4.999e-7,
|
| 1983 |
+
5e-7,
|
| 1984 |
+
5.0001e-7,
|
| 1985 |
+
6e-7,
|
| 1986 |
+
9.999e-7,
|
| 1987 |
+
1e-6,
|
| 1988 |
+
1.0001e-6,
|
| 1989 |
+
2e-6,
|
| 1990 |
+
4.999e-6,
|
| 1991 |
+
5e-6,
|
| 1992 |
+
5.0001e-6,
|
| 1993 |
+
6e-6,
|
| 1994 |
+
]
|
| 1995 |
+
}
|
| 1996 |
+
)
|
| 1997 |
+
|
| 1998 |
+
expected_output = {
|
| 1999 |
+
(0, 6): " col1\n"
|
| 2000 |
+
"0 9.999000e-08\n"
|
| 2001 |
+
"1 1.000000e-07\n"
|
| 2002 |
+
"2 1.000100e-07\n"
|
| 2003 |
+
"3 2.000000e-07\n"
|
| 2004 |
+
"4 4.999000e-07\n"
|
| 2005 |
+
"5 5.000000e-07",
|
| 2006 |
+
(1, 6): " col1\n"
|
| 2007 |
+
"1 1.000000e-07\n"
|
| 2008 |
+
"2 1.000100e-07\n"
|
| 2009 |
+
"3 2.000000e-07\n"
|
| 2010 |
+
"4 4.999000e-07\n"
|
| 2011 |
+
"5 5.000000e-07",
|
| 2012 |
+
(1, 8): " col1\n"
|
| 2013 |
+
"1 1.000000e-07\n"
|
| 2014 |
+
"2 1.000100e-07\n"
|
| 2015 |
+
"3 2.000000e-07\n"
|
| 2016 |
+
"4 4.999000e-07\n"
|
| 2017 |
+
"5 5.000000e-07\n"
|
| 2018 |
+
"6 5.000100e-07\n"
|
| 2019 |
+
"7 6.000000e-07",
|
| 2020 |
+
(8, 16): " col1\n"
|
| 2021 |
+
"8 9.999000e-07\n"
|
| 2022 |
+
"9 1.000000e-06\n"
|
| 2023 |
+
"10 1.000100e-06\n"
|
| 2024 |
+
"11 2.000000e-06\n"
|
| 2025 |
+
"12 4.999000e-06\n"
|
| 2026 |
+
"13 5.000000e-06\n"
|
| 2027 |
+
"14 5.000100e-06\n"
|
| 2028 |
+
"15 6.000000e-06",
|
| 2029 |
+
(9, 16): " col1\n"
|
| 2030 |
+
"9 0.000001\n"
|
| 2031 |
+
"10 0.000001\n"
|
| 2032 |
+
"11 0.000002\n"
|
| 2033 |
+
"12 0.000005\n"
|
| 2034 |
+
"13 0.000005\n"
|
| 2035 |
+
"14 0.000005\n"
|
| 2036 |
+
"15 0.000006",
|
| 2037 |
+
}
|
| 2038 |
+
|
| 2039 |
+
for (start, stop), v in expected_output.items():
|
| 2040 |
+
assert str(d[start:stop]) == v
|
| 2041 |
+
|
| 2042 |
+
def test_too_long(self):
|
| 2043 |
+
# GH 10451
|
| 2044 |
+
with option_context("display.precision", 4):
|
| 2045 |
+
# need both a number > 1e6 and something that normally formats to
|
| 2046 |
+
# having length > display.precision + 6
|
| 2047 |
+
df = DataFrame({"x": [12345.6789]})
|
| 2048 |
+
assert str(df) == " x\n0 12345.6789"
|
| 2049 |
+
df = DataFrame({"x": [2e6]})
|
| 2050 |
+
assert str(df) == " x\n0 2000000.0"
|
| 2051 |
+
df = DataFrame({"x": [12345.6789, 2e6]})
|
| 2052 |
+
assert str(df) == " x\n0 1.2346e+04\n1 2.0000e+06"
|
| 2053 |
+
|
| 2054 |
+
|
| 2055 |
+
class TestTimedelta64Formatter:
|
| 2056 |
+
def test_days(self):
|
| 2057 |
+
x = pd.to_timedelta([*list(range(5)), NaT], unit="D")._values
|
| 2058 |
+
result = fmt._Timedelta64Formatter(x).get_result()
|
| 2059 |
+
assert result[0].strip() == "0 days"
|
| 2060 |
+
assert result[1].strip() == "1 days"
|
| 2061 |
+
|
| 2062 |
+
result = fmt._Timedelta64Formatter(x[1:2]).get_result()
|
| 2063 |
+
assert result[0].strip() == "1 days"
|
| 2064 |
+
|
| 2065 |
+
result = fmt._Timedelta64Formatter(x).get_result()
|
| 2066 |
+
assert result[0].strip() == "0 days"
|
| 2067 |
+
assert result[1].strip() == "1 days"
|
| 2068 |
+
|
| 2069 |
+
result = fmt._Timedelta64Formatter(x[1:2]).get_result()
|
| 2070 |
+
assert result[0].strip() == "1 days"
|
| 2071 |
+
|
| 2072 |
+
def test_days_neg(self):
|
| 2073 |
+
x = pd.to_timedelta([*list(range(5)), NaT], unit="D")._values
|
| 2074 |
+
result = fmt._Timedelta64Formatter(-x).get_result()
|
| 2075 |
+
assert result[0].strip() == "0 days"
|
| 2076 |
+
assert result[1].strip() == "-1 days"
|
| 2077 |
+
|
| 2078 |
+
def test_subdays(self):
|
| 2079 |
+
y = pd.to_timedelta([*list(range(5)), NaT], unit="s")._values
|
| 2080 |
+
result = fmt._Timedelta64Formatter(y).get_result()
|
| 2081 |
+
assert result[0].strip() == "0 days 00:00:00"
|
| 2082 |
+
assert result[1].strip() == "0 days 00:00:01"
|
| 2083 |
+
|
| 2084 |
+
def test_subdays_neg(self):
|
| 2085 |
+
y = pd.to_timedelta([*list(range(5)), NaT], unit="s")._values
|
| 2086 |
+
result = fmt._Timedelta64Formatter(-y).get_result()
|
| 2087 |
+
assert result[0].strip() == "0 days 00:00:00"
|
| 2088 |
+
assert result[1].strip() == "-1 days +23:59:59"
|
| 2089 |
+
|
| 2090 |
+
def test_zero(self):
|
| 2091 |
+
x = pd.to_timedelta([*list(range(1)), NaT], unit="D")._values
|
| 2092 |
+
result = fmt._Timedelta64Formatter(x).get_result()
|
| 2093 |
+
assert result[0].strip() == "0 days"
|
| 2094 |
+
|
| 2095 |
+
x = pd.to_timedelta(list(range(1)), unit="D")._values
|
| 2096 |
+
result = fmt._Timedelta64Formatter(x).get_result()
|
| 2097 |
+
assert result[0].strip() == "0 days"
|
| 2098 |
+
|
| 2099 |
+
|
| 2100 |
+
class TestDatetime64Formatter:
|
| 2101 |
+
def test_mixed(self):
|
| 2102 |
+
x = Series([datetime(2013, 1, 1), datetime(2013, 1, 1, 12), NaT])._values
|
| 2103 |
+
result = fmt._Datetime64Formatter(x).get_result()
|
| 2104 |
+
assert result[0].strip() == "2013-01-01 00:00:00"
|
| 2105 |
+
assert result[1].strip() == "2013-01-01 12:00:00"
|
| 2106 |
+
|
| 2107 |
+
def test_dates(self):
|
| 2108 |
+
x = Series([datetime(2013, 1, 1), datetime(2013, 1, 2), NaT])._values
|
| 2109 |
+
result = fmt._Datetime64Formatter(x).get_result()
|
| 2110 |
+
assert result[0].strip() == "2013-01-01"
|
| 2111 |
+
assert result[1].strip() == "2013-01-02"
|
| 2112 |
+
|
| 2113 |
+
def test_date_nanos(self):
|
| 2114 |
+
x = Series([Timestamp(200)])._values
|
| 2115 |
+
result = fmt._Datetime64Formatter(x).get_result()
|
| 2116 |
+
assert result[0].strip() == "1970-01-01 00:00:00.000000200"
|
| 2117 |
+
|
| 2118 |
+
def test_dates_display(self):
|
| 2119 |
+
# 10170
|
| 2120 |
+
# make sure that we are consistently display date formatting
|
| 2121 |
+
x = Series(date_range("20130101 09:00:00", periods=5, freq="D"))
|
| 2122 |
+
x.iloc[1] = np.nan
|
| 2123 |
+
result = fmt._Datetime64Formatter(x._values).get_result()
|
| 2124 |
+
assert result[0].strip() == "2013-01-01 09:00:00"
|
| 2125 |
+
assert result[1].strip() == "NaT"
|
| 2126 |
+
assert result[4].strip() == "2013-01-05 09:00:00"
|
| 2127 |
+
|
| 2128 |
+
x = Series(date_range("20130101 09:00:00", periods=5, freq="s"))
|
| 2129 |
+
x.iloc[1] = np.nan
|
| 2130 |
+
result = fmt._Datetime64Formatter(x._values).get_result()
|
| 2131 |
+
assert result[0].strip() == "2013-01-01 09:00:00"
|
| 2132 |
+
assert result[1].strip() == "NaT"
|
| 2133 |
+
assert result[4].strip() == "2013-01-01 09:00:04"
|
| 2134 |
+
|
| 2135 |
+
x = Series(date_range("20130101 09:00:00", periods=5, freq="ms"))
|
| 2136 |
+
x.iloc[1] = np.nan
|
| 2137 |
+
result = fmt._Datetime64Formatter(x._values).get_result()
|
| 2138 |
+
assert result[0].strip() == "2013-01-01 09:00:00.000"
|
| 2139 |
+
assert result[1].strip() == "NaT"
|
| 2140 |
+
assert result[4].strip() == "2013-01-01 09:00:00.004"
|
| 2141 |
+
|
| 2142 |
+
x = Series(date_range("20130101 09:00:00", periods=5, freq="us"))
|
| 2143 |
+
x.iloc[1] = np.nan
|
| 2144 |
+
result = fmt._Datetime64Formatter(x._values).get_result()
|
| 2145 |
+
assert result[0].strip() == "2013-01-01 09:00:00.000000"
|
| 2146 |
+
assert result[1].strip() == "NaT"
|
| 2147 |
+
assert result[4].strip() == "2013-01-01 09:00:00.000004"
|
| 2148 |
+
|
| 2149 |
+
x = Series(date_range("20130101 09:00:00", periods=5, freq="ns"))
|
| 2150 |
+
x.iloc[1] = np.nan
|
| 2151 |
+
result = fmt._Datetime64Formatter(x._values).get_result()
|
| 2152 |
+
assert result[0].strip() == "2013-01-01 09:00:00.000000000"
|
| 2153 |
+
assert result[1].strip() == "NaT"
|
| 2154 |
+
assert result[4].strip() == "2013-01-01 09:00:00.000000004"
|
| 2155 |
+
|
| 2156 |
+
def test_datetime64formatter_yearmonth(self):
|
| 2157 |
+
x = Series([datetime(2016, 1, 1), datetime(2016, 2, 2)])._values
|
| 2158 |
+
|
| 2159 |
+
def format_func(x):
|
| 2160 |
+
return x.strftime("%Y-%m")
|
| 2161 |
+
|
| 2162 |
+
formatter = fmt._Datetime64Formatter(x, formatter=format_func)
|
| 2163 |
+
result = formatter.get_result()
|
| 2164 |
+
assert result == ["2016-01", "2016-02"]
|
| 2165 |
+
|
| 2166 |
+
def test_datetime64formatter_hoursecond(self):
|
| 2167 |
+
x = Series(
|
| 2168 |
+
pd.to_datetime(["10:10:10.100", "12:12:12.120"], format="%H:%M:%S.%f")
|
| 2169 |
+
)._values
|
| 2170 |
+
|
| 2171 |
+
def format_func(x):
|
| 2172 |
+
return x.strftime("%H:%M")
|
| 2173 |
+
|
| 2174 |
+
formatter = fmt._Datetime64Formatter(x, formatter=format_func)
|
| 2175 |
+
result = formatter.get_result()
|
| 2176 |
+
assert result == ["10:10", "12:12"]
|
| 2177 |
+
|
| 2178 |
+
def test_datetime64formatter_tz_ms(self):
|
| 2179 |
+
x = (
|
| 2180 |
+
Series(
|
| 2181 |
+
np.array(["2999-01-01", "2999-01-02", "NaT"], dtype="datetime64[ms]")
|
| 2182 |
+
)
|
| 2183 |
+
.dt.tz_localize("US/Pacific")
|
| 2184 |
+
._values
|
| 2185 |
+
)
|
| 2186 |
+
result = fmt._Datetime64TZFormatter(x).get_result()
|
| 2187 |
+
assert result[0].strip() == "2999-01-01 00:00:00-08:00"
|
| 2188 |
+
assert result[1].strip() == "2999-01-02 00:00:00-08:00"
|
| 2189 |
+
|
| 2190 |
+
|
| 2191 |
+
class TestFormatPercentiles:
|
| 2192 |
+
@pytest.mark.parametrize(
|
| 2193 |
+
"percentiles, expected",
|
| 2194 |
+
[
|
| 2195 |
+
(
|
| 2196 |
+
[0.01999, 0.02001, 0.5, 0.666666, 0.9999],
|
| 2197 |
+
["1.999%", "2.001%", "50%", "66.667%", "99.99%"],
|
| 2198 |
+
),
|
| 2199 |
+
(
|
| 2200 |
+
[0, 0.5, 0.02001, 0.5, 0.666666, 0.9999],
|
| 2201 |
+
["0%", "50%", "2.0%", "50%", "66.67%", "99.99%"],
|
| 2202 |
+
),
|
| 2203 |
+
([0.281, 0.29, 0.57, 0.58], ["28.1%", "29%", "57%", "58%"]),
|
| 2204 |
+
([0.28, 0.29, 0.57, 0.58], ["28%", "29%", "57%", "58%"]),
|
| 2205 |
+
(
|
| 2206 |
+
[0.9, 0.99, 0.999, 0.9999, 0.99999],
|
| 2207 |
+
["90%", "99%", "99.9%", "99.99%", "99.999%"],
|
| 2208 |
+
),
|
| 2209 |
+
],
|
| 2210 |
+
)
|
| 2211 |
+
def test_format_percentiles(self, percentiles, expected):
|
| 2212 |
+
result = fmt.format_percentiles(percentiles)
|
| 2213 |
+
assert result == expected
|
| 2214 |
+
|
| 2215 |
+
@pytest.mark.parametrize(
|
| 2216 |
+
"percentiles",
|
| 2217 |
+
[
|
| 2218 |
+
([0.1, np.nan, 0.5]),
|
| 2219 |
+
([-0.001, 0.1, 0.5]),
|
| 2220 |
+
([2, 0.1, 0.5]),
|
| 2221 |
+
([0.1, 0.5, "a"]),
|
| 2222 |
+
],
|
| 2223 |
+
)
|
| 2224 |
+
def test_error_format_percentiles(self, percentiles):
|
| 2225 |
+
msg = r"percentiles should all be in the interval \[0,1\]"
|
| 2226 |
+
with pytest.raises(ValueError, match=msg):
|
| 2227 |
+
fmt.format_percentiles(percentiles)
|
| 2228 |
+
|
| 2229 |
+
def test_format_percentiles_integer_idx(self):
|
| 2230 |
+
# Issue #26660
|
| 2231 |
+
result = fmt.format_percentiles(np.linspace(0, 1, 10 + 1))
|
| 2232 |
+
expected = [
|
| 2233 |
+
"0%",
|
| 2234 |
+
"10%",
|
| 2235 |
+
"20%",
|
| 2236 |
+
"30%",
|
| 2237 |
+
"40%",
|
| 2238 |
+
"50%",
|
| 2239 |
+
"60%",
|
| 2240 |
+
"70%",
|
| 2241 |
+
"80%",
|
| 2242 |
+
"90%",
|
| 2243 |
+
"100%",
|
| 2244 |
+
]
|
| 2245 |
+
assert result == expected
|
| 2246 |
+
|
| 2247 |
+
|
| 2248 |
+
@pytest.mark.parametrize("method", ["to_string", "to_html", "to_latex"])
|
| 2249 |
+
@pytest.mark.parametrize(
|
| 2250 |
+
"encoding, data",
|
| 2251 |
+
[(None, "abc"), ("utf-8", "abc"), ("gbk", "造成输出中文显示乱码"), ("foo", "abc")],
|
| 2252 |
+
)
|
| 2253 |
+
@pytest.mark.parametrize("filepath_or_buffer_id", ["string", "pathlike", "buffer"])
|
| 2254 |
+
def test_filepath_or_buffer_arg(
|
| 2255 |
+
method,
|
| 2256 |
+
tmp_path,
|
| 2257 |
+
encoding,
|
| 2258 |
+
data,
|
| 2259 |
+
filepath_or_buffer_id,
|
| 2260 |
+
):
|
| 2261 |
+
if filepath_or_buffer_id == "buffer":
|
| 2262 |
+
filepath_or_buffer = StringIO()
|
| 2263 |
+
elif filepath_or_buffer_id == "pathlike":
|
| 2264 |
+
filepath_or_buffer = tmp_path / "foo"
|
| 2265 |
+
else:
|
| 2266 |
+
filepath_or_buffer = str(tmp_path / "foo")
|
| 2267 |
+
|
| 2268 |
+
df = DataFrame([data])
|
| 2269 |
+
if method in ["to_latex"]: # uses styler implementation
|
| 2270 |
+
pytest.importorskip("jinja2")
|
| 2271 |
+
|
| 2272 |
+
if filepath_or_buffer_id not in ["string", "pathlike"] and encoding is not None:
|
| 2273 |
+
with pytest.raises(
|
| 2274 |
+
ValueError, match="buf is not a file name and encoding is specified."
|
| 2275 |
+
):
|
| 2276 |
+
getattr(df, method)(buf=filepath_or_buffer, encoding=encoding)
|
| 2277 |
+
elif encoding == "foo":
|
| 2278 |
+
with pytest.raises(LookupError, match="unknown encoding"):
|
| 2279 |
+
getattr(df, method)(buf=filepath_or_buffer, encoding=encoding)
|
| 2280 |
+
else:
|
| 2281 |
+
expected = getattr(df, method)()
|
| 2282 |
+
getattr(df, method)(buf=filepath_or_buffer, encoding=encoding)
|
| 2283 |
+
encoding = encoding or "utf-8"
|
| 2284 |
+
if filepath_or_buffer_id == "string":
|
| 2285 |
+
with open(filepath_or_buffer, encoding=encoding) as f:
|
| 2286 |
+
result = f.read()
|
| 2287 |
+
elif filepath_or_buffer_id == "pathlike":
|
| 2288 |
+
result = filepath_or_buffer.read_text(encoding=encoding)
|
| 2289 |
+
elif filepath_or_buffer_id == "buffer":
|
| 2290 |
+
result = filepath_or_buffer.getvalue()
|
| 2291 |
+
assert result == expected
|
| 2292 |
+
if filepath_or_buffer_id == "buffer":
|
| 2293 |
+
assert not filepath_or_buffer.closed
|
| 2294 |
+
|
| 2295 |
+
|
| 2296 |
+
@pytest.mark.parametrize("method", ["to_string", "to_html", "to_latex"])
|
| 2297 |
+
def test_filepath_or_buffer_bad_arg_raises(float_frame, method):
|
| 2298 |
+
if method in ["to_latex"]: # uses styler implementation
|
| 2299 |
+
pytest.importorskip("jinja2")
|
| 2300 |
+
msg = "buf is not a file name and it has no write method"
|
| 2301 |
+
with pytest.raises(TypeError, match=msg):
|
| 2302 |
+
getattr(float_frame, method)(buf=object())
|
.venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_ipython_compat.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
|
| 3 |
+
import pandas._config.config as cf
|
| 4 |
+
|
| 5 |
+
from pandas import (
|
| 6 |
+
DataFrame,
|
| 7 |
+
MultiIndex,
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class TestTableSchemaRepr:
|
| 12 |
+
def test_publishes(self, ip):
|
| 13 |
+
ipython = ip.instance(config=ip.config)
|
| 14 |
+
df = DataFrame({"A": [1, 2]})
|
| 15 |
+
objects = [df["A"], df] # dataframe / series
|
| 16 |
+
expected_keys = [
|
| 17 |
+
{"text/plain", "application/vnd.dataresource+json"},
|
| 18 |
+
{"text/plain", "text/html", "application/vnd.dataresource+json"},
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
opt = cf.option_context("display.html.table_schema", True)
|
| 22 |
+
last_obj = None
|
| 23 |
+
for obj, expected in zip(objects, expected_keys):
|
| 24 |
+
last_obj = obj
|
| 25 |
+
with cf.option_context("display.html.table_schema", True):
|
| 26 |
+
# Can't reuse opt on all systems GH#58055
|
| 27 |
+
formatted = ipython.display_formatter.format(obj)
|
| 28 |
+
assert set(formatted[0].keys()) == expected
|
| 29 |
+
|
| 30 |
+
with_latex = cf.option_context("styler.render.repr", "latex")
|
| 31 |
+
|
| 32 |
+
with opt, with_latex:
|
| 33 |
+
formatted = ipython.display_formatter.format(last_obj)
|
| 34 |
+
|
| 35 |
+
expected = {
|
| 36 |
+
"text/plain",
|
| 37 |
+
"text/html",
|
| 38 |
+
"text/latex",
|
| 39 |
+
"application/vnd.dataresource+json",
|
| 40 |
+
}
|
| 41 |
+
assert set(formatted[0].keys()) == expected
|
| 42 |
+
|
| 43 |
+
def test_publishes_not_implemented(self, ip):
|
| 44 |
+
# column MultiIndex
|
| 45 |
+
# GH#15996
|
| 46 |
+
midx = MultiIndex.from_product([["A", "B"], ["a", "b", "c"]])
|
| 47 |
+
df = DataFrame(
|
| 48 |
+
np.random.default_rng(2).standard_normal((5, len(midx))), columns=midx
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
opt = cf.option_context("display.html.table_schema", True)
|
| 52 |
+
|
| 53 |
+
with opt:
|
| 54 |
+
formatted = ip.instance(config=ip.config).display_formatter.format(df)
|
| 55 |
+
|
| 56 |
+
expected = {"text/plain", "text/html"}
|
| 57 |
+
assert set(formatted[0].keys()) == expected
|
| 58 |
+
|
| 59 |
+
def test_config_on(self):
|
| 60 |
+
df = DataFrame({"A": [1, 2]})
|
| 61 |
+
with cf.option_context("display.html.table_schema", True):
|
| 62 |
+
result = df._repr_data_resource_()
|
| 63 |
+
|
| 64 |
+
assert result is not None
|
| 65 |
+
|
| 66 |
+
def test_config_default_off(self):
|
| 67 |
+
df = DataFrame({"A": [1, 2]})
|
| 68 |
+
with cf.option_context("display.html.table_schema", False):
|
| 69 |
+
result = df._repr_data_resource_()
|
| 70 |
+
|
| 71 |
+
assert result is None
|
| 72 |
+
|
| 73 |
+
def test_enable_data_resource_formatter(self, ip):
|
| 74 |
+
# GH#10491
|
| 75 |
+
formatters = ip.instance(config=ip.config).display_formatter.formatters
|
| 76 |
+
mimetype = "application/vnd.dataresource+json"
|
| 77 |
+
|
| 78 |
+
with cf.option_context("display.html.table_schema", True):
|
| 79 |
+
assert "application/vnd.dataresource+json" in formatters
|
| 80 |
+
assert formatters[mimetype].enabled
|
| 81 |
+
|
| 82 |
+
# still there, just disabled
|
| 83 |
+
assert "application/vnd.dataresource+json" in formatters
|
| 84 |
+
assert not formatters[mimetype].enabled
|
| 85 |
+
|
| 86 |
+
# able to re-set
|
| 87 |
+
with cf.option_context("display.html.table_schema", True):
|
| 88 |
+
assert "application/vnd.dataresource+json" in formatters
|
| 89 |
+
assert formatters[mimetype].enabled
|
| 90 |
+
# smoke test that it works
|
| 91 |
+
ip.instance(config=ip.config).display_formatter.format(cf)
|
.venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_printing.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Note! This file is aimed specifically at pandas.io.formats.printing utility
|
| 2 |
+
# functions, not the general printing of pandas objects.
|
| 3 |
+
from collections.abc import Mapping
|
| 4 |
+
import string
|
| 5 |
+
|
| 6 |
+
import pytest
|
| 7 |
+
|
| 8 |
+
import pandas._config.config as cf
|
| 9 |
+
|
| 10 |
+
import pandas as pd
|
| 11 |
+
|
| 12 |
+
from pandas.io.formats import printing
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@pytest.mark.parametrize(
|
| 16 |
+
"input_names, expected_names",
|
| 17 |
+
[
|
| 18 |
+
(["'a b"], "['\\'a b']"), # Escape leading quote
|
| 19 |
+
(["test's b"], "['test\\'s b']"), # Escape apostrophe
|
| 20 |
+
(["'test' b"], "['\\'test\\' b']"), # Escape surrounding quotes
|
| 21 |
+
(["test b'"], "['test b\\'']"), # Escape single quote
|
| 22 |
+
(["test\n' b"], "['test\\n\\' b']"), # Escape quotes, preserve newline
|
| 23 |
+
],
|
| 24 |
+
)
|
| 25 |
+
def test_formatted_index_names(input_names, expected_names):
|
| 26 |
+
# GH#60190
|
| 27 |
+
df = pd.DataFrame({name: [1, 2, 3] for name in input_names}).set_index(input_names)
|
| 28 |
+
formatted_names = str(df.index.names)
|
| 29 |
+
|
| 30 |
+
assert formatted_names == expected_names
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def test_adjoin():
|
| 34 |
+
data = [["a", "b", "c"], ["dd", "ee", "ff"], ["ggg", "hhh", "iii"]]
|
| 35 |
+
expected = "a dd ggg\nb ee hhh\nc ff iii"
|
| 36 |
+
|
| 37 |
+
adjoined = printing.adjoin(2, *data)
|
| 38 |
+
|
| 39 |
+
assert adjoined == expected
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class MyMapping(Mapping):
|
| 43 |
+
def __getitem__(self, key):
|
| 44 |
+
return 4
|
| 45 |
+
|
| 46 |
+
def __iter__(self):
|
| 47 |
+
return iter(["a", "b"])
|
| 48 |
+
|
| 49 |
+
def __len__(self):
|
| 50 |
+
return 2
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class TestPPrintThing:
|
| 54 |
+
def test_repr_binary_type(self):
|
| 55 |
+
letters = string.ascii_letters
|
| 56 |
+
try:
|
| 57 |
+
raw = bytes(letters, encoding=cf.get_option("display.encoding"))
|
| 58 |
+
except TypeError:
|
| 59 |
+
raw = bytes(letters)
|
| 60 |
+
b = str(raw.decode("utf-8"))
|
| 61 |
+
res = printing.pprint_thing(b, quote_strings=True)
|
| 62 |
+
assert res == repr(b)
|
| 63 |
+
res = printing.pprint_thing(b, quote_strings=False)
|
| 64 |
+
assert res == b
|
| 65 |
+
|
| 66 |
+
def test_repr_obeys_max_seq_limit(self):
|
| 67 |
+
with cf.option_context("display.max_seq_items", 2000):
|
| 68 |
+
assert len(printing.pprint_thing(list(range(1000)))) > 1000
|
| 69 |
+
|
| 70 |
+
with cf.option_context("display.max_seq_items", 5):
|
| 71 |
+
assert len(printing.pprint_thing(list(range(1000)))) < 100
|
| 72 |
+
|
| 73 |
+
with cf.option_context("display.max_seq_items", 1):
|
| 74 |
+
assert len(printing.pprint_thing(list(range(1000)))) < 9
|
| 75 |
+
|
| 76 |
+
def test_repr_set(self):
|
| 77 |
+
assert printing.pprint_thing({1}) == "{1}"
|
| 78 |
+
|
| 79 |
+
def test_repr_dict(self):
|
| 80 |
+
assert printing.pprint_thing({"a": 4, "b": 4}) == "{'a': 4, 'b': 4}"
|
| 81 |
+
|
| 82 |
+
def test_repr_mapping(self):
|
| 83 |
+
assert printing.pprint_thing(MyMapping()) == "{'a': 4, 'b': 4}"
|
| 84 |
+
|
| 85 |
+
def test_repr_frozenset(self):
|
| 86 |
+
assert printing.pprint_thing(frozenset([1, 2])) == "frozenset({1, 2})"
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
class TestFormatBase:
|
| 90 |
+
def test_adjoin(self):
|
| 91 |
+
data = [["a", "b", "c"], ["dd", "ee", "ff"], ["ggg", "hhh", "iii"]]
|
| 92 |
+
expected = "a dd ggg\nb ee hhh\nc ff iii"
|
| 93 |
+
|
| 94 |
+
adjoined = printing.adjoin(2, *data)
|
| 95 |
+
|
| 96 |
+
assert adjoined == expected
|
| 97 |
+
|
| 98 |
+
def test_adjoin_unicode(self):
|
| 99 |
+
data = [["あ", "b", "c"], ["dd", "ええ", "ff"], ["ggg", "hhh", "いいい"]]
|
| 100 |
+
expected = "あ dd ggg\nb ええ hhh\nc ff いいい"
|
| 101 |
+
adjoined = printing.adjoin(2, *data)
|
| 102 |
+
assert adjoined == expected
|
| 103 |
+
|
| 104 |
+
adj = printing._EastAsianTextAdjustment()
|
| 105 |
+
|
| 106 |
+
expected = """あ dd ggg
|
| 107 |
+
b ええ hhh
|
| 108 |
+
c ff いいい"""
|
| 109 |
+
|
| 110 |
+
adjoined = adj.adjoin(2, *data)
|
| 111 |
+
assert adjoined == expected
|
| 112 |
+
cols = adjoined.split("\n")
|
| 113 |
+
assert adj.len(cols[0]) == 13
|
| 114 |
+
assert adj.len(cols[1]) == 13
|
| 115 |
+
assert adj.len(cols[2]) == 16
|
| 116 |
+
|
| 117 |
+
expected = """あ dd ggg
|
| 118 |
+
b ええ hhh
|
| 119 |
+
c ff いいい"""
|
| 120 |
+
|
| 121 |
+
adjoined = adj.adjoin(7, *data)
|
| 122 |
+
assert adjoined == expected
|
| 123 |
+
cols = adjoined.split("\n")
|
| 124 |
+
assert adj.len(cols[0]) == 23
|
| 125 |
+
assert adj.len(cols[1]) == 23
|
| 126 |
+
assert adj.len(cols[2]) == 26
|
| 127 |
+
|
| 128 |
+
def test_justify(self):
|
| 129 |
+
adj = printing._EastAsianTextAdjustment()
|
| 130 |
+
|
| 131 |
+
def just(x, *args, **kwargs):
|
| 132 |
+
# wrapper to test single str
|
| 133 |
+
return adj.justify([x], *args, **kwargs)[0]
|
| 134 |
+
|
| 135 |
+
assert just("abc", 5, mode="left") == "abc "
|
| 136 |
+
assert just("abc", 5, mode="center") == " abc "
|
| 137 |
+
assert just("abc", 5, mode="right") == " abc"
|
| 138 |
+
assert just("abc", 5, mode="left") == "abc "
|
| 139 |
+
assert just("abc", 5, mode="center") == " abc "
|
| 140 |
+
assert just("abc", 5, mode="right") == " abc"
|
| 141 |
+
|
| 142 |
+
assert just("パンダ", 5, mode="left") == "パンダ"
|
| 143 |
+
assert just("パンダ", 5, mode="center") == "パンダ"
|
| 144 |
+
assert just("パンダ", 5, mode="right") == "パンダ"
|
| 145 |
+
|
| 146 |
+
assert just("パンダ", 10, mode="left") == "パンダ "
|
| 147 |
+
assert just("パンダ", 10, mode="center") == " パンダ "
|
| 148 |
+
assert just("パンダ", 10, mode="right") == " パンダ"
|
| 149 |
+
|
| 150 |
+
def test_east_asian_len(self):
|
| 151 |
+
adj = printing._EastAsianTextAdjustment()
|
| 152 |
+
|
| 153 |
+
assert adj.len("abc") == 3
|
| 154 |
+
assert adj.len("abc") == 3
|
| 155 |
+
|
| 156 |
+
assert adj.len("パンダ") == 6
|
| 157 |
+
assert adj.len("パンダ") == 5
|
| 158 |
+
assert adj.len("パンダpanda") == 11
|
| 159 |
+
assert adj.len("パンダpanda") == 10
|
| 160 |
+
|
| 161 |
+
def test_ambiguous_width(self):
|
| 162 |
+
adj = printing._EastAsianTextAdjustment()
|
| 163 |
+
assert adj.len("¡¡ab") == 4
|
| 164 |
+
|
| 165 |
+
with cf.option_context("display.unicode.ambiguous_as_wide", True):
|
| 166 |
+
adj = printing._EastAsianTextAdjustment()
|
| 167 |
+
assert adj.len("¡¡ab") == 6
|
| 168 |
+
|
| 169 |
+
data = [["あ", "b", "c"], ["dd", "ええ", "ff"], ["ggg", "¡¡ab", "いいい"]]
|
| 170 |
+
expected = "あ dd ggg \nb ええ ¡¡ab\nc ff いいい"
|
| 171 |
+
adjoined = adj.adjoin(2, *data)
|
| 172 |
+
assert adjoined == expected
|
.venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_csv.py
ADDED
|
@@ -0,0 +1,886 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
from zipfile import ZipFile
|
| 5 |
+
|
| 6 |
+
from _csv import Error
|
| 7 |
+
import numpy as np
|
| 8 |
+
import pytest
|
| 9 |
+
|
| 10 |
+
import pandas as pd
|
| 11 |
+
from pandas import (
|
| 12 |
+
DataFrame,
|
| 13 |
+
Index,
|
| 14 |
+
compat,
|
| 15 |
+
)
|
| 16 |
+
import pandas._testing as tm
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class TestToCSV:
|
| 20 |
+
def test_to_csv_with_single_column(self, temp_file):
|
| 21 |
+
# see gh-18676, https://bugs.python.org/issue32255
|
| 22 |
+
#
|
| 23 |
+
# Python's CSV library adds an extraneous '""'
|
| 24 |
+
# before the newline when the NaN-value is in
|
| 25 |
+
# the first row. Otherwise, only the newline
|
| 26 |
+
# character is added. This behavior is inconsistent
|
| 27 |
+
# and was patched in https://bugs.python.org/pull_request4672.
|
| 28 |
+
df1 = DataFrame([None, 1])
|
| 29 |
+
expected1 = """\
|
| 30 |
+
""
|
| 31 |
+
1.0
|
| 32 |
+
"""
|
| 33 |
+
df1.to_csv(temp_file, header=None, index=None)
|
| 34 |
+
with open(temp_file, encoding="utf-8") as f:
|
| 35 |
+
assert f.read() == expected1
|
| 36 |
+
|
| 37 |
+
df2 = DataFrame([1, None])
|
| 38 |
+
expected2 = """\
|
| 39 |
+
1.0
|
| 40 |
+
""
|
| 41 |
+
"""
|
| 42 |
+
df2.to_csv(temp_file, header=None, index=None)
|
| 43 |
+
with open(temp_file, encoding="utf-8") as f:
|
| 44 |
+
assert f.read() == expected2
|
| 45 |
+
|
| 46 |
+
def test_to_csv_default_encoding(self, temp_file):
|
| 47 |
+
# GH17097
|
| 48 |
+
df = DataFrame({"col": ["AAAAA", "ÄÄÄÄÄ", "ßßßßß", "聞聞聞聞聞"]})
|
| 49 |
+
|
| 50 |
+
# the default to_csv encoding is uft-8.
|
| 51 |
+
df.to_csv(temp_file)
|
| 52 |
+
tm.assert_frame_equal(pd.read_csv(temp_file, index_col=0), df)
|
| 53 |
+
|
| 54 |
+
def test_to_csv_quotechar(self, temp_file):
|
| 55 |
+
df = DataFrame({"col": [1, 2]})
|
| 56 |
+
expected = """\
|
| 57 |
+
"","col"
|
| 58 |
+
"0","1"
|
| 59 |
+
"1","2"
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
df.to_csv(temp_file, quoting=1) # 1=QUOTE_ALL
|
| 63 |
+
with open(temp_file, encoding="utf-8") as f:
|
| 64 |
+
assert f.read() == expected
|
| 65 |
+
|
| 66 |
+
expected = """\
|
| 67 |
+
$$,$col$
|
| 68 |
+
$0$,$1$
|
| 69 |
+
$1$,$2$
|
| 70 |
+
"""
|
| 71 |
+
|
| 72 |
+
df.to_csv(temp_file, quoting=1, quotechar="$")
|
| 73 |
+
with open(temp_file, encoding="utf-8") as f:
|
| 74 |
+
assert f.read() == expected
|
| 75 |
+
|
| 76 |
+
with pytest.raises(TypeError, match="quotechar"):
|
| 77 |
+
df.to_csv(temp_file, quoting=1, quotechar=None)
|
| 78 |
+
|
| 79 |
+
def test_to_csv_doublequote(self, temp_file):
|
| 80 |
+
df = DataFrame({"col": ['a"a', '"bb"']})
|
| 81 |
+
expected = '''\
|
| 82 |
+
"","col"
|
| 83 |
+
"0","a""a"
|
| 84 |
+
"1","""bb"""
|
| 85 |
+
'''
|
| 86 |
+
|
| 87 |
+
df.to_csv(temp_file, quoting=1, doublequote=True) # QUOTE_ALL
|
| 88 |
+
with open(temp_file, encoding="utf-8") as f:
|
| 89 |
+
assert f.read() == expected
|
| 90 |
+
|
| 91 |
+
with pytest.raises(Error, match="escapechar"):
|
| 92 |
+
df.to_csv(temp_file, doublequote=False) # no escapechar set
|
| 93 |
+
|
| 94 |
+
def test_to_csv_escapechar(self, temp_file):
|
| 95 |
+
df = DataFrame({"col": ['a"a', '"bb"']})
|
| 96 |
+
expected = """\
|
| 97 |
+
"","col"
|
| 98 |
+
"0","a\\"a"
|
| 99 |
+
"1","\\"bb\\""
|
| 100 |
+
"""
|
| 101 |
+
|
| 102 |
+
df.to_csv(temp_file, quoting=1, doublequote=False, escapechar="\\")
|
| 103 |
+
with open(temp_file, encoding="utf-8") as f:
|
| 104 |
+
assert f.read() == expected
|
| 105 |
+
|
| 106 |
+
df = DataFrame({"col": ["a,a", ",bb,"]})
|
| 107 |
+
expected = """\
|
| 108 |
+
,col
|
| 109 |
+
0,a\\,a
|
| 110 |
+
1,\\,bb\\,
|
| 111 |
+
"""
|
| 112 |
+
|
| 113 |
+
df.to_csv(temp_file, quoting=3, escapechar="\\") # QUOTE_NONE
|
| 114 |
+
with open(temp_file, encoding="utf-8") as f:
|
| 115 |
+
assert f.read() == expected
|
| 116 |
+
|
| 117 |
+
def test_csv_to_string(self):
|
| 118 |
+
df = DataFrame({"col": [1, 2]})
|
| 119 |
+
expected_rows = [",col", "0,1", "1,2"]
|
| 120 |
+
expected = tm.convert_rows_list_to_csv_str(expected_rows)
|
| 121 |
+
assert df.to_csv() == expected
|
| 122 |
+
|
| 123 |
+
def test_to_csv_decimal(self):
|
| 124 |
+
# see gh-781
|
| 125 |
+
df = DataFrame({"col1": [1], "col2": ["a"], "col3": [10.1]})
|
| 126 |
+
|
| 127 |
+
expected_rows = [",col1,col2,col3", "0,1,a,10.1"]
|
| 128 |
+
expected_default = tm.convert_rows_list_to_csv_str(expected_rows)
|
| 129 |
+
assert df.to_csv() == expected_default
|
| 130 |
+
|
| 131 |
+
expected_rows = [";col1;col2;col3", "0;1;a;10,1"]
|
| 132 |
+
expected_european_excel = tm.convert_rows_list_to_csv_str(expected_rows)
|
| 133 |
+
assert df.to_csv(decimal=",", sep=";") == expected_european_excel
|
| 134 |
+
|
| 135 |
+
expected_rows = [",col1,col2,col3", "0,1,a,10.10"]
|
| 136 |
+
expected_float_format_default = tm.convert_rows_list_to_csv_str(expected_rows)
|
| 137 |
+
assert df.to_csv(float_format="%.2f") == expected_float_format_default
|
| 138 |
+
|
| 139 |
+
expected_rows = [";col1;col2;col3", "0;1;a;10,10"]
|
| 140 |
+
expected_float_format = tm.convert_rows_list_to_csv_str(expected_rows)
|
| 141 |
+
assert (
|
| 142 |
+
df.to_csv(decimal=",", sep=";", float_format="%.2f")
|
| 143 |
+
== expected_float_format
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
# see gh-11553: testing if decimal is taken into account for '0.0'
|
| 147 |
+
df = DataFrame({"a": [0, 1.1], "b": [2.2, 3.3], "c": 1})
|
| 148 |
+
|
| 149 |
+
expected_rows = ["a,b,c", "0^0,2^2,1", "1^1,3^3,1"]
|
| 150 |
+
expected = tm.convert_rows_list_to_csv_str(expected_rows)
|
| 151 |
+
assert df.to_csv(index=False, decimal="^") == expected
|
| 152 |
+
|
| 153 |
+
# same but for an index
|
| 154 |
+
assert df.set_index("a").to_csv(decimal="^") == expected
|
| 155 |
+
|
| 156 |
+
# same for a multi-index
|
| 157 |
+
assert df.set_index(["a", "b"]).to_csv(decimal="^") == expected
|
| 158 |
+
|
| 159 |
+
def test_to_csv_float_format(self):
|
| 160 |
+
# testing if float_format is taken into account for the index
|
| 161 |
+
# GH 11553
|
| 162 |
+
df = DataFrame({"a": [0, 1], "b": [2.2, 3.3], "c": 1})
|
| 163 |
+
|
| 164 |
+
expected_rows = ["a,b,c", "0,2.20,1", "1,3.30,1"]
|
| 165 |
+
expected = tm.convert_rows_list_to_csv_str(expected_rows)
|
| 166 |
+
assert df.set_index("a").to_csv(float_format="%.2f") == expected
|
| 167 |
+
|
| 168 |
+
# same for a multi-index
|
| 169 |
+
assert df.set_index(["a", "b"]).to_csv(float_format="%.2f") == expected
|
| 170 |
+
|
| 171 |
+
def test_to_csv_na_rep(self):
|
| 172 |
+
# see gh-11553
|
| 173 |
+
#
|
| 174 |
+
# Testing if NaN values are correctly represented in the index.
|
| 175 |
+
df = DataFrame({"a": [0, np.nan], "b": [0, 1], "c": [2, 3]})
|
| 176 |
+
expected_rows = ["a,b,c", "0.0,0,2", "_,1,3"]
|
| 177 |
+
expected = tm.convert_rows_list_to_csv_str(expected_rows)
|
| 178 |
+
|
| 179 |
+
assert df.set_index("a").to_csv(na_rep="_") == expected
|
| 180 |
+
assert df.set_index(["a", "b"]).to_csv(na_rep="_") == expected
|
| 181 |
+
|
| 182 |
+
# now with an index containing only NaNs
|
| 183 |
+
df = DataFrame({"a": np.nan, "b": [0, 1], "c": [2, 3]})
|
| 184 |
+
expected_rows = ["a,b,c", "_,0,2", "_,1,3"]
|
| 185 |
+
expected = tm.convert_rows_list_to_csv_str(expected_rows)
|
| 186 |
+
|
| 187 |
+
assert df.set_index("a").to_csv(na_rep="_") == expected
|
| 188 |
+
assert df.set_index(["a", "b"]).to_csv(na_rep="_") == expected
|
| 189 |
+
|
| 190 |
+
# check if na_rep parameter does not break anything when no NaN
|
| 191 |
+
df = DataFrame({"a": 0, "b": [0, 1], "c": [2, 3]})
|
| 192 |
+
expected_rows = ["a,b,c", "0,0,2", "0,1,3"]
|
| 193 |
+
expected = tm.convert_rows_list_to_csv_str(expected_rows)
|
| 194 |
+
|
| 195 |
+
assert df.set_index("a").to_csv(na_rep="_") == expected
|
| 196 |
+
assert df.set_index(["a", "b"]).to_csv(na_rep="_") == expected
|
| 197 |
+
|
| 198 |
+
csv = pd.Series(["a", pd.NA, "c"]).to_csv(na_rep="ZZZZZ")
|
| 199 |
+
expected = tm.convert_rows_list_to_csv_str([",0", "0,a", "1,ZZZZZ", "2,c"])
|
| 200 |
+
assert expected == csv
|
| 201 |
+
|
| 202 |
+
def test_to_csv_na_rep_nullable_string(self, nullable_string_dtype):
|
| 203 |
+
# GH 29975
|
| 204 |
+
# Make sure full na_rep shows up when a dtype is provided
|
| 205 |
+
expected = tm.convert_rows_list_to_csv_str([",0", "0,a", "1,ZZZZZ", "2,c"])
|
| 206 |
+
csv = pd.Series(["a", pd.NA, "c"], dtype=nullable_string_dtype).to_csv(
|
| 207 |
+
na_rep="ZZZZZ"
|
| 208 |
+
)
|
| 209 |
+
assert expected == csv
|
| 210 |
+
|
| 211 |
+
def test_to_csv_date_format(self):
|
| 212 |
+
# GH 10209
|
| 213 |
+
df_sec = DataFrame({"A": pd.date_range("20130101", periods=5, freq="s")})
|
| 214 |
+
df_day = DataFrame({"A": pd.date_range("20130101", periods=5, freq="D")})
|
| 215 |
+
|
| 216 |
+
expected_rows = [
|
| 217 |
+
",A",
|
| 218 |
+
"0,2013-01-01 00:00:00",
|
| 219 |
+
"1,2013-01-01 00:00:01",
|
| 220 |
+
"2,2013-01-01 00:00:02",
|
| 221 |
+
"3,2013-01-01 00:00:03",
|
| 222 |
+
"4,2013-01-01 00:00:04",
|
| 223 |
+
]
|
| 224 |
+
expected_default_sec = tm.convert_rows_list_to_csv_str(expected_rows)
|
| 225 |
+
assert df_sec.to_csv() == expected_default_sec
|
| 226 |
+
|
| 227 |
+
expected_rows = [
|
| 228 |
+
",A",
|
| 229 |
+
"0,2013-01-01 00:00:00",
|
| 230 |
+
"1,2013-01-02 00:00:00",
|
| 231 |
+
"2,2013-01-03 00:00:00",
|
| 232 |
+
"3,2013-01-04 00:00:00",
|
| 233 |
+
"4,2013-01-05 00:00:00",
|
| 234 |
+
]
|
| 235 |
+
expected_ymdhms_day = tm.convert_rows_list_to_csv_str(expected_rows)
|
| 236 |
+
assert df_day.to_csv(date_format="%Y-%m-%d %H:%M:%S") == expected_ymdhms_day
|
| 237 |
+
|
| 238 |
+
expected_rows = [
|
| 239 |
+
",A",
|
| 240 |
+
"0,2013-01-01",
|
| 241 |
+
"1,2013-01-01",
|
| 242 |
+
"2,2013-01-01",
|
| 243 |
+
"3,2013-01-01",
|
| 244 |
+
"4,2013-01-01",
|
| 245 |
+
]
|
| 246 |
+
expected_ymd_sec = tm.convert_rows_list_to_csv_str(expected_rows)
|
| 247 |
+
assert df_sec.to_csv(date_format="%Y-%m-%d") == expected_ymd_sec
|
| 248 |
+
|
| 249 |
+
expected_rows = [
|
| 250 |
+
",A",
|
| 251 |
+
"0,2013-01-01",
|
| 252 |
+
"1,2013-01-02",
|
| 253 |
+
"2,2013-01-03",
|
| 254 |
+
"3,2013-01-04",
|
| 255 |
+
"4,2013-01-05",
|
| 256 |
+
]
|
| 257 |
+
expected_default_day = tm.convert_rows_list_to_csv_str(expected_rows)
|
| 258 |
+
assert df_day.to_csv() == expected_default_day
|
| 259 |
+
assert df_day.to_csv(date_format="%Y-%m-%d") == expected_default_day
|
| 260 |
+
|
| 261 |
+
# see gh-7791
|
| 262 |
+
#
|
| 263 |
+
# Testing if date_format parameter is taken into account
|
| 264 |
+
# for multi-indexed DataFrames.
|
| 265 |
+
df_sec["B"] = 0
|
| 266 |
+
df_sec["C"] = 1
|
| 267 |
+
|
| 268 |
+
expected_rows = ["A,B,C", "2013-01-01,0,1.0"]
|
| 269 |
+
expected_ymd_sec = tm.convert_rows_list_to_csv_str(expected_rows)
|
| 270 |
+
|
| 271 |
+
df_sec_grouped = df_sec.groupby([pd.Grouper(key="A", freq="1h"), "B"])
|
| 272 |
+
assert df_sec_grouped.mean().to_csv(date_format="%Y-%m-%d") == expected_ymd_sec
|
| 273 |
+
|
| 274 |
+
def test_to_csv_different_datetime_formats(self):
|
| 275 |
+
# GH#21734
|
| 276 |
+
df = DataFrame(
|
| 277 |
+
{
|
| 278 |
+
"date": pd.to_datetime("1970-01-01"),
|
| 279 |
+
"datetime": pd.date_range("1970-01-01", periods=2, freq="h"),
|
| 280 |
+
}
|
| 281 |
+
)
|
| 282 |
+
expected_rows = [
|
| 283 |
+
"date,datetime",
|
| 284 |
+
"1970-01-01,1970-01-01 00:00:00",
|
| 285 |
+
"1970-01-01,1970-01-01 01:00:00",
|
| 286 |
+
]
|
| 287 |
+
expected = tm.convert_rows_list_to_csv_str(expected_rows)
|
| 288 |
+
assert df.to_csv(index=False) == expected
|
| 289 |
+
|
| 290 |
+
def test_to_csv_date_format_in_categorical(self):
|
| 291 |
+
# GH#40754
|
| 292 |
+
ser = pd.Series(pd.to_datetime(["2021-03-27", pd.NaT], format="%Y-%m-%d"))
|
| 293 |
+
ser = ser.astype("category")
|
| 294 |
+
expected = tm.convert_rows_list_to_csv_str(["0", "2021-03-27", '""'])
|
| 295 |
+
assert ser.to_csv(index=False) == expected
|
| 296 |
+
|
| 297 |
+
ser = pd.Series(
|
| 298 |
+
pd.date_range(
|
| 299 |
+
start="2021-03-27", freq="D", periods=1, tz="Europe/Berlin"
|
| 300 |
+
).append(pd.DatetimeIndex([pd.NaT]))
|
| 301 |
+
)
|
| 302 |
+
ser = ser.astype("category")
|
| 303 |
+
assert ser.to_csv(index=False, date_format="%Y-%m-%d") == expected
|
| 304 |
+
|
| 305 |
+
def test_to_csv_float_ea_float_format(self):
|
| 306 |
+
# GH#45991
|
| 307 |
+
df = DataFrame({"a": [1.1, 2.02, pd.NA, 6.000006], "b": "c"})
|
| 308 |
+
df["a"] = df["a"].astype("Float64")
|
| 309 |
+
result = df.to_csv(index=False, float_format="%.5f")
|
| 310 |
+
expected = tm.convert_rows_list_to_csv_str(
|
| 311 |
+
["a,b", "1.10000,c", "2.02000,c", ",c", "6.00001,c"]
|
| 312 |
+
)
|
| 313 |
+
assert result == expected
|
| 314 |
+
|
| 315 |
+
def test_to_csv_float_ea_no_float_format(self):
|
| 316 |
+
# GH#45991
|
| 317 |
+
df = DataFrame({"a": [1.1, 2.02, pd.NA, 6.000006], "b": "c"})
|
| 318 |
+
df["a"] = df["a"].astype("Float64")
|
| 319 |
+
result = df.to_csv(index=False)
|
| 320 |
+
expected = tm.convert_rows_list_to_csv_str(
|
| 321 |
+
["a,b", "1.1,c", "2.02,c", ",c", "6.000006,c"]
|
| 322 |
+
)
|
| 323 |
+
assert result == expected
|
| 324 |
+
|
| 325 |
+
def test_to_csv_float_ea_nan_distinguish(self, using_nan_is_na):
|
| 326 |
+
# GH#61617, GH#65227 - to_csv should not crash when FloatingArray
|
| 327 |
+
# contains unmasked NaN (with distinguish_nan_and_na=True)
|
| 328 |
+
df = DataFrame({"a": pd.array([np.nan, pd.NA, 3.0], dtype="Float64"), "b": "c"})
|
| 329 |
+
result = df.to_csv(index=False)
|
| 330 |
+
if using_nan_is_na:
|
| 331 |
+
expected = tm.convert_rows_list_to_csv_str(["a,b", ",c", ",c", "3.0,c"])
|
| 332 |
+
else:
|
| 333 |
+
expected = tm.convert_rows_list_to_csv_str(["a,b", "nan,c", ",c", "3.0,c"])
|
| 334 |
+
assert result == expected
|
| 335 |
+
|
| 336 |
+
def test_to_csv_float_ea_nan_distinguish_series(self, using_nan_is_na):
|
| 337 |
+
# GH#65227 - Series.to_csv with FloatingArray containing both NaN and NA
|
| 338 |
+
ser = pd.Series((1, pd.NA, 0), index=["a", "b", "c"], dtype="Float64", name="x")
|
| 339 |
+
ser = ser / ser
|
| 340 |
+
result = ser.to_csv()
|
| 341 |
+
if using_nan_is_na:
|
| 342 |
+
expected = tm.convert_rows_list_to_csv_str([",x", "a,1.0", "b,", "c,"])
|
| 343 |
+
else:
|
| 344 |
+
expected = tm.convert_rows_list_to_csv_str([",x", "a,1.0", "b,", "c,nan"])
|
| 345 |
+
assert result == expected
|
| 346 |
+
|
| 347 |
+
def test_to_csv_multi_index(self):
|
| 348 |
+
# see gh-6618
|
| 349 |
+
df = DataFrame([1], columns=pd.MultiIndex.from_arrays([[1], [2]]))
|
| 350 |
+
|
| 351 |
+
exp_rows = [",1", ",2", "0,1"]
|
| 352 |
+
exp = tm.convert_rows_list_to_csv_str(exp_rows)
|
| 353 |
+
assert df.to_csv() == exp
|
| 354 |
+
|
| 355 |
+
exp_rows = ["1", "2", "1"]
|
| 356 |
+
exp = tm.convert_rows_list_to_csv_str(exp_rows)
|
| 357 |
+
assert df.to_csv(index=False) == exp
|
| 358 |
+
|
| 359 |
+
df = DataFrame(
|
| 360 |
+
[1],
|
| 361 |
+
columns=pd.MultiIndex.from_arrays([[1], [2]]),
|
| 362 |
+
index=pd.MultiIndex.from_arrays([[1], [2]]),
|
| 363 |
+
)
|
| 364 |
+
|
| 365 |
+
exp_rows = [",,1", ",,2", "1,2,1"]
|
| 366 |
+
exp = tm.convert_rows_list_to_csv_str(exp_rows)
|
| 367 |
+
assert df.to_csv() == exp
|
| 368 |
+
|
| 369 |
+
exp_rows = ["1", "2", "1"]
|
| 370 |
+
exp = tm.convert_rows_list_to_csv_str(exp_rows)
|
| 371 |
+
assert df.to_csv(index=False) == exp
|
| 372 |
+
|
| 373 |
+
df = DataFrame([1], columns=pd.MultiIndex.from_arrays([["foo"], ["bar"]]))
|
| 374 |
+
|
| 375 |
+
exp_rows = [",foo", ",bar", "0,1"]
|
| 376 |
+
exp = tm.convert_rows_list_to_csv_str(exp_rows)
|
| 377 |
+
assert df.to_csv() == exp
|
| 378 |
+
|
| 379 |
+
exp_rows = ["foo", "bar", "1"]
|
| 380 |
+
exp = tm.convert_rows_list_to_csv_str(exp_rows)
|
| 381 |
+
assert df.to_csv(index=False) == exp
|
| 382 |
+
|
| 383 |
+
@pytest.mark.parametrize(
|
| 384 |
+
"ind,expected",
|
| 385 |
+
[
|
| 386 |
+
(
|
| 387 |
+
pd.MultiIndex(levels=[[1.0]], codes=[[0]], names=["x"]),
|
| 388 |
+
"x,data\n1.0,1\n",
|
| 389 |
+
),
|
| 390 |
+
(
|
| 391 |
+
pd.MultiIndex(
|
| 392 |
+
levels=[[1.0], [2.0]], codes=[[0], [0]], names=["x", "y"]
|
| 393 |
+
),
|
| 394 |
+
"x,y,data\n1.0,2.0,1\n",
|
| 395 |
+
),
|
| 396 |
+
],
|
| 397 |
+
)
|
| 398 |
+
def test_to_csv_single_level_multi_index(self, ind, expected, frame_or_series):
|
| 399 |
+
# see gh-19589
|
| 400 |
+
obj = frame_or_series(pd.Series([1], ind, name="data"))
|
| 401 |
+
|
| 402 |
+
result = obj.to_csv(lineterminator="\n", header=True)
|
| 403 |
+
assert result == expected
|
| 404 |
+
|
| 405 |
+
def test_to_csv_string_array_ascii(self, temp_file):
|
| 406 |
+
# GH 10813
|
| 407 |
+
str_array = [{"names": ["foo", "bar"]}, {"names": ["baz", "qux"]}]
|
| 408 |
+
df = DataFrame(str_array)
|
| 409 |
+
expected_ascii = """\
|
| 410 |
+
,names
|
| 411 |
+
0,"['foo', 'bar']"
|
| 412 |
+
1,"['baz', 'qux']"
|
| 413 |
+
"""
|
| 414 |
+
df.to_csv(temp_file, encoding="ascii")
|
| 415 |
+
with open(temp_file, encoding="utf-8") as f:
|
| 416 |
+
assert f.read() == expected_ascii
|
| 417 |
+
|
| 418 |
+
def test_to_csv_string_array_utf8(self, temp_file):
|
| 419 |
+
# GH 10813
|
| 420 |
+
str_array = [{"names": ["foo", "bar"]}, {"names": ["baz", "qux"]}]
|
| 421 |
+
df = DataFrame(str_array)
|
| 422 |
+
expected_utf8 = """\
|
| 423 |
+
,names
|
| 424 |
+
0,"['foo', 'bar']"
|
| 425 |
+
1,"['baz', 'qux']"
|
| 426 |
+
"""
|
| 427 |
+
df.to_csv(temp_file, encoding="utf-8")
|
| 428 |
+
with open(temp_file, encoding="utf-8") as f:
|
| 429 |
+
assert f.read() == expected_utf8
|
| 430 |
+
|
| 431 |
+
def test_to_csv_string_with_lf(self, temp_file):
|
| 432 |
+
# GH 20353
|
| 433 |
+
data = {"int": [1, 2, 3], "str_lf": ["abc", "d\nef", "g\nh\n\ni"]}
|
| 434 |
+
df = DataFrame(data)
|
| 435 |
+
|
| 436 |
+
# case 1: The default line terminator(=os.linesep)(PR 21406)
|
| 437 |
+
os_linesep = os.linesep.encode("utf-8")
|
| 438 |
+
expected_noarg = (
|
| 439 |
+
b"int,str_lf"
|
| 440 |
+
+ os_linesep
|
| 441 |
+
+ b"1,abc"
|
| 442 |
+
+ os_linesep
|
| 443 |
+
+ b'2,"d\nef"'
|
| 444 |
+
+ os_linesep
|
| 445 |
+
+ b'3,"g\nh\n\ni"'
|
| 446 |
+
+ os_linesep
|
| 447 |
+
)
|
| 448 |
+
df.to_csv(temp_file, index=False)
|
| 449 |
+
with open(temp_file, "rb") as f:
|
| 450 |
+
assert f.read() == expected_noarg
|
| 451 |
+
|
| 452 |
+
# case 2: LF as line terminator
|
| 453 |
+
expected_lf = b'int,str_lf\n1,abc\n2,"d\nef"\n3,"g\nh\n\ni"\n'
|
| 454 |
+
df.to_csv(temp_file, lineterminator="\n", index=False)
|
| 455 |
+
with open(temp_file, "rb") as f:
|
| 456 |
+
assert f.read() == expected_lf
|
| 457 |
+
|
| 458 |
+
# case 3: CRLF as line terminator
|
| 459 |
+
# 'lineterminator' should not change inner element
|
| 460 |
+
expected_crlf = b'int,str_lf\r\n1,abc\r\n2,"d\nef"\r\n3,"g\nh\n\ni"\r\n'
|
| 461 |
+
df.to_csv(temp_file, lineterminator="\r\n", index=False)
|
| 462 |
+
with open(temp_file, "rb") as f:
|
| 463 |
+
assert f.read() == expected_crlf
|
| 464 |
+
|
| 465 |
+
def test_to_csv_string_with_crlf(self, temp_file):
|
| 466 |
+
# GH 20353
|
| 467 |
+
data = {"int": [1, 2, 3], "str_crlf": ["abc", "d\r\nef", "g\r\nh\r\n\r\ni"]}
|
| 468 |
+
df = DataFrame(data)
|
| 469 |
+
# case 1: The default line terminator(=os.linesep)(PR 21406)
|
| 470 |
+
os_linesep = os.linesep.encode("utf-8")
|
| 471 |
+
expected_noarg = (
|
| 472 |
+
b"int,str_crlf"
|
| 473 |
+
+ os_linesep
|
| 474 |
+
+ b"1,abc"
|
| 475 |
+
+ os_linesep
|
| 476 |
+
+ b'2,"d\r\nef"'
|
| 477 |
+
+ os_linesep
|
| 478 |
+
+ b'3,"g\r\nh\r\n\r\ni"'
|
| 479 |
+
+ os_linesep
|
| 480 |
+
)
|
| 481 |
+
df.to_csv(temp_file, index=False)
|
| 482 |
+
with open(temp_file, "rb") as f:
|
| 483 |
+
assert f.read() == expected_noarg
|
| 484 |
+
|
| 485 |
+
# case 2: LF as line terminator
|
| 486 |
+
expected_lf = b'int,str_crlf\n1,abc\n2,"d\r\nef"\n3,"g\r\nh\r\n\r\ni"\n'
|
| 487 |
+
df.to_csv(temp_file, lineterminator="\n", index=False)
|
| 488 |
+
with open(temp_file, "rb") as f:
|
| 489 |
+
assert f.read() == expected_lf
|
| 490 |
+
|
| 491 |
+
# case 3: CRLF as line terminator
|
| 492 |
+
# 'lineterminator' should not change inner element
|
| 493 |
+
expected_crlf = (
|
| 494 |
+
b'int,str_crlf\r\n1,abc\r\n2,"d\r\nef"\r\n3,"g\r\nh\r\n\r\ni"\r\n'
|
| 495 |
+
)
|
| 496 |
+
df.to_csv(temp_file, lineterminator="\r\n", index=False)
|
| 497 |
+
with open(temp_file, "rb") as f:
|
| 498 |
+
assert f.read() == expected_crlf
|
| 499 |
+
|
| 500 |
+
def test_to_csv_stdout_file(self, capsys):
|
| 501 |
+
# GH 21561
|
| 502 |
+
df = DataFrame([["foo", "bar"], ["baz", "qux"]], columns=["name_1", "name_2"])
|
| 503 |
+
expected_rows = [",name_1,name_2", "0,foo,bar", "1,baz,qux"]
|
| 504 |
+
expected_ascii = tm.convert_rows_list_to_csv_str(expected_rows)
|
| 505 |
+
|
| 506 |
+
df.to_csv(sys.stdout, encoding="ascii")
|
| 507 |
+
captured = capsys.readouterr()
|
| 508 |
+
|
| 509 |
+
assert captured.out == expected_ascii
|
| 510 |
+
assert not sys.stdout.closed
|
| 511 |
+
|
| 512 |
+
@pytest.mark.xfail(
|
| 513 |
+
compat.is_platform_windows(),
|
| 514 |
+
reason=(
|
| 515 |
+
"Especially in Windows, file stream should not be passed"
|
| 516 |
+
"to csv writer without newline='' option."
|
| 517 |
+
"(https://docs.python.org/3/library/csv.html#csv.writer)"
|
| 518 |
+
),
|
| 519 |
+
)
|
| 520 |
+
def test_to_csv_write_to_open_file(self, temp_file):
|
| 521 |
+
# GH 21696
|
| 522 |
+
df = DataFrame({"a": ["x", "y", "z"]})
|
| 523 |
+
expected = """\
|
| 524 |
+
manual header
|
| 525 |
+
x
|
| 526 |
+
y
|
| 527 |
+
z
|
| 528 |
+
"""
|
| 529 |
+
with open(temp_file, "w", encoding="utf-8") as f:
|
| 530 |
+
f.write("manual header\n")
|
| 531 |
+
df.to_csv(f, header=None, index=None)
|
| 532 |
+
with open(temp_file, encoding="utf-8") as f:
|
| 533 |
+
assert f.read() == expected
|
| 534 |
+
|
| 535 |
+
def test_to_csv_write_to_open_file_with_newline_py3(self, temp_file):
|
| 536 |
+
# see gh-21696
|
| 537 |
+
# see gh-20353
|
| 538 |
+
df = DataFrame({"a": ["x", "y", "z"]})
|
| 539 |
+
expected_rows = ["x", "y", "z"]
|
| 540 |
+
expected = "manual header\n" + tm.convert_rows_list_to_csv_str(expected_rows)
|
| 541 |
+
|
| 542 |
+
with open(temp_file, "w", newline="", encoding="utf-8") as f:
|
| 543 |
+
f.write("manual header\n")
|
| 544 |
+
df.to_csv(f, header=None, index=None)
|
| 545 |
+
|
| 546 |
+
with open(temp_file, "rb") as f:
|
| 547 |
+
assert f.read() == bytes(expected, "utf-8")
|
| 548 |
+
|
| 549 |
+
@pytest.mark.parametrize("to_infer", [True, False])
|
| 550 |
+
@pytest.mark.parametrize("read_infer", [True, False])
|
| 551 |
+
def test_to_csv_compression(
|
| 552 |
+
self,
|
| 553 |
+
compression_only,
|
| 554 |
+
read_infer,
|
| 555 |
+
to_infer,
|
| 556 |
+
compression_to_extension,
|
| 557 |
+
temp_file,
|
| 558 |
+
):
|
| 559 |
+
# see gh-15008
|
| 560 |
+
compression = compression_only
|
| 561 |
+
|
| 562 |
+
df = DataFrame({"A": [1]})
|
| 563 |
+
|
| 564 |
+
to_compression = "infer" if to_infer else compression
|
| 565 |
+
read_compression = "infer" if read_infer else compression
|
| 566 |
+
|
| 567 |
+
path_ext = str(temp_file) + "." + compression_to_extension[compression]
|
| 568 |
+
df.to_csv(path_ext, compression=to_compression)
|
| 569 |
+
result = pd.read_csv(path_ext, index_col=0, compression=read_compression)
|
| 570 |
+
tm.assert_frame_equal(result, df)
|
| 571 |
+
|
| 572 |
+
def test_to_csv_compression_dict(self, compression_only, temp_file):
|
| 573 |
+
# GH 26023
|
| 574 |
+
method = compression_only
|
| 575 |
+
df = DataFrame({"ABC": [1]})
|
| 576 |
+
extension = {
|
| 577 |
+
"gzip": "gz",
|
| 578 |
+
"zstd": "zst",
|
| 579 |
+
}.get(method, method)
|
| 580 |
+
|
| 581 |
+
path = str(temp_file) + "." + extension
|
| 582 |
+
df.to_csv(path, compression={"method": method})
|
| 583 |
+
read_df = pd.read_csv(path, index_col=0)
|
| 584 |
+
tm.assert_frame_equal(read_df, df)
|
| 585 |
+
|
| 586 |
+
def test_to_csv_compression_dict_no_method_raises(self, temp_file):
|
| 587 |
+
# GH 26023
|
| 588 |
+
df = DataFrame({"ABC": [1]})
|
| 589 |
+
compression = {"some_option": True}
|
| 590 |
+
msg = "must have key 'method'"
|
| 591 |
+
|
| 592 |
+
with pytest.raises(ValueError, match=msg):
|
| 593 |
+
df.to_csv(temp_file, compression=compression)
|
| 594 |
+
|
| 595 |
+
@pytest.mark.parametrize("compression", ["zip", "infer"])
|
| 596 |
+
@pytest.mark.parametrize("archive_name", ["test_to_csv.csv", "test_to_csv.zip"])
|
| 597 |
+
def test_to_csv_zip_arguments(self, compression, archive_name, temp_file):
|
| 598 |
+
# GH 26023
|
| 599 |
+
df = DataFrame({"ABC": [1]})
|
| 600 |
+
|
| 601 |
+
path = str(temp_file) + ".zip"
|
| 602 |
+
df.to_csv(
|
| 603 |
+
path, compression={"method": compression, "archive_name": archive_name}
|
| 604 |
+
)
|
| 605 |
+
with ZipFile(path) as zp:
|
| 606 |
+
assert len(zp.filelist) == 1
|
| 607 |
+
archived_file = zp.filelist[0].filename
|
| 608 |
+
assert archived_file == archive_name
|
| 609 |
+
|
| 610 |
+
@pytest.mark.parametrize(
|
| 611 |
+
"filename,expected_arcname",
|
| 612 |
+
[
|
| 613 |
+
("archive.csv", "archive.csv"),
|
| 614 |
+
("archive.tsv", "archive.tsv"),
|
| 615 |
+
("archive.csv.zip", "archive.csv"),
|
| 616 |
+
("archive.tsv.zip", "archive.tsv"),
|
| 617 |
+
("archive.zip", "archive"),
|
| 618 |
+
],
|
| 619 |
+
)
|
| 620 |
+
def test_to_csv_zip_infer_name(self, tmp_path, filename, expected_arcname):
|
| 621 |
+
# GH 39465
|
| 622 |
+
df = DataFrame({"ABC": [1]})
|
| 623 |
+
path = tmp_path / filename
|
| 624 |
+
df.to_csv(path, compression="zip")
|
| 625 |
+
with ZipFile(path) as zp:
|
| 626 |
+
assert len(zp.filelist) == 1
|
| 627 |
+
archived_file = zp.filelist[0].filename
|
| 628 |
+
assert archived_file == expected_arcname
|
| 629 |
+
|
| 630 |
+
@pytest.mark.parametrize("df_new_type", ["Int64"])
|
| 631 |
+
def test_to_csv_na_rep_long_string(self, df_new_type):
|
| 632 |
+
# see gh-25099
|
| 633 |
+
df = DataFrame({"c": [pd.NA] * 3})
|
| 634 |
+
df = df.astype(df_new_type)
|
| 635 |
+
expected_rows = ["c", "mynull", "mynull", "mynull"]
|
| 636 |
+
expected = tm.convert_rows_list_to_csv_str(expected_rows)
|
| 637 |
+
|
| 638 |
+
result = df.to_csv(index=False, na_rep="mynull", encoding="ascii")
|
| 639 |
+
|
| 640 |
+
assert expected == result
|
| 641 |
+
|
| 642 |
+
def test_to_csv_timedelta_precision(self):
|
| 643 |
+
# GH 6783
|
| 644 |
+
s = pd.Series([1, 1]).astype("timedelta64[ns]")
|
| 645 |
+
buf = io.StringIO()
|
| 646 |
+
s.to_csv(buf)
|
| 647 |
+
result = buf.getvalue()
|
| 648 |
+
expected_rows = [
|
| 649 |
+
",0",
|
| 650 |
+
"0,0 days 00:00:00.000000001",
|
| 651 |
+
"1,0 days 00:00:00.000000001",
|
| 652 |
+
]
|
| 653 |
+
expected = tm.convert_rows_list_to_csv_str(expected_rows)
|
| 654 |
+
assert result == expected
|
| 655 |
+
|
| 656 |
+
def test_na_rep_truncated(self):
|
| 657 |
+
# https://github.com/pandas-dev/pandas/issues/31447
|
| 658 |
+
result = pd.Series(range(8, 12)).to_csv(na_rep="-")
|
| 659 |
+
expected = tm.convert_rows_list_to_csv_str([",0", "0,8", "1,9", "2,10", "3,11"])
|
| 660 |
+
assert result == expected
|
| 661 |
+
|
| 662 |
+
result = pd.Series([True, False]).to_csv(na_rep="nan")
|
| 663 |
+
expected = tm.convert_rows_list_to_csv_str([",0", "0,True", "1,False"])
|
| 664 |
+
assert result == expected
|
| 665 |
+
|
| 666 |
+
result = pd.Series([1.1, 2.2]).to_csv(na_rep=".")
|
| 667 |
+
expected = tm.convert_rows_list_to_csv_str([",0", "0,1.1", "1,2.2"])
|
| 668 |
+
assert result == expected
|
| 669 |
+
|
| 670 |
+
@pytest.mark.parametrize("errors", ["surrogatepass", "ignore", "replace"])
|
| 671 |
+
def test_to_csv_errors(self, errors, temp_file):
|
| 672 |
+
# GH 22610
|
| 673 |
+
data = ["\ud800foo"]
|
| 674 |
+
ser = pd.Series(data, index=Index(data, dtype=object), dtype=object)
|
| 675 |
+
|
| 676 |
+
ser.to_csv(temp_file, errors=errors)
|
| 677 |
+
# No use in reading back the data as it is not the same anymore
|
| 678 |
+
# due to the error handling
|
| 679 |
+
|
| 680 |
+
@pytest.mark.parametrize("mode", ["wb", "w"])
|
| 681 |
+
def test_to_csv_binary_handle(self, mode, temp_file):
|
| 682 |
+
"""
|
| 683 |
+
Binary file objects should work (if 'mode' contains a 'b') or even without
|
| 684 |
+
it in most cases.
|
| 685 |
+
|
| 686 |
+
GH 35058 and GH 19827
|
| 687 |
+
"""
|
| 688 |
+
df = DataFrame(
|
| 689 |
+
1.1 * np.arange(120).reshape((30, 4)),
|
| 690 |
+
columns=Index(list("ABCD")),
|
| 691 |
+
index=Index([f"i-{i}" for i in range(30)]),
|
| 692 |
+
)
|
| 693 |
+
|
| 694 |
+
with open(temp_file, mode="w+b") as handle:
|
| 695 |
+
df.to_csv(handle, mode=mode)
|
| 696 |
+
tm.assert_frame_equal(df, pd.read_csv(temp_file, index_col=0))
|
| 697 |
+
|
| 698 |
+
@pytest.mark.parametrize("mode", ["wb", "w"])
|
| 699 |
+
def test_to_csv_encoding_binary_handle(self, mode, temp_file):
|
| 700 |
+
"""
|
| 701 |
+
Binary file objects should honor a specified encoding.
|
| 702 |
+
|
| 703 |
+
GH 23854 and GH 13068 with binary handles
|
| 704 |
+
"""
|
| 705 |
+
# example from GH 23854
|
| 706 |
+
content = "a, b, 🐟".encode("utf-8-sig")
|
| 707 |
+
buffer = io.BytesIO(content)
|
| 708 |
+
df = pd.read_csv(buffer, encoding="utf-8-sig")
|
| 709 |
+
|
| 710 |
+
buffer = io.BytesIO()
|
| 711 |
+
df.to_csv(buffer, mode=mode, encoding="utf-8-sig", index=False)
|
| 712 |
+
buffer.seek(0) # tests whether file handle wasn't closed
|
| 713 |
+
assert buffer.getvalue().startswith(content)
|
| 714 |
+
|
| 715 |
+
# example from GH 13068
|
| 716 |
+
with open(temp_file, "w+b") as handle:
|
| 717 |
+
DataFrame().to_csv(handle, mode=mode, encoding="utf-8-sig")
|
| 718 |
+
|
| 719 |
+
handle.seek(0)
|
| 720 |
+
assert handle.read().startswith(b'\xef\xbb\xbf""')
|
| 721 |
+
|
| 722 |
+
|
| 723 |
+
def test_to_csv_iterative_compression_name(compression, temp_file):
|
| 724 |
+
# GH 38714
|
| 725 |
+
df = DataFrame(
|
| 726 |
+
1.1 * np.arange(120).reshape((30, 4)),
|
| 727 |
+
columns=Index(list("ABCD")),
|
| 728 |
+
index=Index([f"i-{i}" for i in range(30)]),
|
| 729 |
+
)
|
| 730 |
+
df.to_csv(temp_file, compression=compression, chunksize=1)
|
| 731 |
+
tm.assert_frame_equal(
|
| 732 |
+
pd.read_csv(temp_file, compression=compression, index_col=0), df
|
| 733 |
+
)
|
| 734 |
+
|
| 735 |
+
|
| 736 |
+
def test_to_csv_iterative_compression_buffer(compression):
|
| 737 |
+
# GH 38714
|
| 738 |
+
df = DataFrame(
|
| 739 |
+
1.1 * np.arange(120).reshape((30, 4)),
|
| 740 |
+
columns=Index(list("ABCD")),
|
| 741 |
+
index=Index([f"i-{i}" for i in range(30)]),
|
| 742 |
+
)
|
| 743 |
+
with io.BytesIO() as buffer:
|
| 744 |
+
df.to_csv(buffer, compression=compression, chunksize=1)
|
| 745 |
+
buffer.seek(0)
|
| 746 |
+
tm.assert_frame_equal(
|
| 747 |
+
pd.read_csv(buffer, compression=compression, index_col=0), df
|
| 748 |
+
)
|
| 749 |
+
assert not buffer.closed
|
| 750 |
+
|
| 751 |
+
|
| 752 |
+
def test_new_style_float_format_basic():
|
| 753 |
+
df = DataFrame({"A": [1234.56789, 9876.54321]})
|
| 754 |
+
result = df.to_csv(float_format="{:.2f}", lineterminator="\n")
|
| 755 |
+
expected = ",A\n0,1234.57\n1,9876.54\n"
|
| 756 |
+
assert result == expected
|
| 757 |
+
|
| 758 |
+
|
| 759 |
+
def test_new_style_float_format_thousands():
|
| 760 |
+
df = DataFrame({"A": [1234.56789, 9876.54321]})
|
| 761 |
+
result = df.to_csv(float_format="{:,.2f}", lineterminator="\n")
|
| 762 |
+
expected = ',A\n0,"1,234.57"\n1,"9,876.54"\n'
|
| 763 |
+
assert result == expected
|
| 764 |
+
|
| 765 |
+
|
| 766 |
+
def test_new_style_scientific_format():
|
| 767 |
+
df = DataFrame({"A": [0.000123, 0.000456]})
|
| 768 |
+
result = df.to_csv(float_format="{:.2e}", lineterminator="\n")
|
| 769 |
+
expected = ",A\n0,1.23e-04\n1,4.56e-04\n"
|
| 770 |
+
assert result == expected
|
| 771 |
+
|
| 772 |
+
|
| 773 |
+
def test_new_style_with_nan():
|
| 774 |
+
df = DataFrame({"A": [1.23, np.nan, 4.56]})
|
| 775 |
+
result = df.to_csv(float_format="{:.2f}", na_rep="NA", lineterminator="\n")
|
| 776 |
+
expected = ",A\n0,1.23\n1,NA\n2,4.56\n"
|
| 777 |
+
assert result == expected
|
| 778 |
+
|
| 779 |
+
|
| 780 |
+
def test_new_style_with_mixed_types():
|
| 781 |
+
df = DataFrame({"A": [1.23, 4.56], "B": ["x", "y"]})
|
| 782 |
+
result = df.to_csv(float_format="{:.2f}", lineterminator="\n")
|
| 783 |
+
expected = ",A,B\n0,1.23,x\n1,4.56,y\n"
|
| 784 |
+
assert result == expected
|
| 785 |
+
|
| 786 |
+
|
| 787 |
+
def test_new_style_with_mixed_types_in_column():
|
| 788 |
+
df = DataFrame({"A": [1.23, "text", 4.56]})
|
| 789 |
+
result = df.to_csv(float_format="{:.2f}", lineterminator="\n")
|
| 790 |
+
expected = ",A\n0,1.23\n1,text\n2,4.56\n"
|
| 791 |
+
assert result == expected
|
| 792 |
+
|
| 793 |
+
|
| 794 |
+
def test_invalid_new_style_format_missing_brace():
|
| 795 |
+
df = DataFrame({"A": [1.23]})
|
| 796 |
+
with pytest.raises(ValueError, match="Invalid new-style format string '{:.2f"):
|
| 797 |
+
df.to_csv(float_format="{:.2f")
|
| 798 |
+
|
| 799 |
+
|
| 800 |
+
def test_invalid_new_style_format_specifier():
|
| 801 |
+
df = DataFrame({"A": [1.23]})
|
| 802 |
+
with pytest.raises(ValueError, match="Invalid new-style format string '{:.2z}'"):
|
| 803 |
+
df.to_csv(float_format="{:.2z}")
|
| 804 |
+
|
| 805 |
+
|
| 806 |
+
def test_old_style_format_compatibility():
|
| 807 |
+
df = DataFrame({"A": [1234.56789, 9876.54321]})
|
| 808 |
+
result = df.to_csv(float_format="%.2f", lineterminator="\n")
|
| 809 |
+
expected = ",A\n0,1234.57\n1,9876.54\n"
|
| 810 |
+
assert result == expected
|
| 811 |
+
|
| 812 |
+
|
| 813 |
+
def test_callable_float_format_compatibility():
|
| 814 |
+
df = DataFrame({"A": [1234.56789, 9876.54321]})
|
| 815 |
+
result = df.to_csv(float_format=lambda x: f"{x:,.2f}", lineterminator="\n")
|
| 816 |
+
expected = ',A\n0,"1,234.57"\n1,"9,876.54"\n'
|
| 817 |
+
assert result == expected
|
| 818 |
+
|
| 819 |
+
|
| 820 |
+
def test_no_float_format():
|
| 821 |
+
df = DataFrame({"A": [1.23, 4.56]})
|
| 822 |
+
result = df.to_csv(float_format=None, lineterminator="\n")
|
| 823 |
+
expected = ",A\n0,1.23\n1,4.56\n"
|
| 824 |
+
assert result == expected
|
| 825 |
+
|
| 826 |
+
|
| 827 |
+
def test_large_numbers():
|
| 828 |
+
df = DataFrame({"A": [1e308, 2e308]})
|
| 829 |
+
result = df.to_csv(float_format="{:.2e}", lineterminator="\n")
|
| 830 |
+
expected = ",A\n0,1.00e+308\n1,inf\n"
|
| 831 |
+
assert result == expected
|
| 832 |
+
|
| 833 |
+
|
| 834 |
+
def test_zero_and_negative():
|
| 835 |
+
df = DataFrame({"A": [0.0, -1.23456]})
|
| 836 |
+
result = df.to_csv(float_format="{:+.2f}", lineterminator="\n")
|
| 837 |
+
expected = ",A\n0,+0.00\n1,-1.23\n"
|
| 838 |
+
assert result == expected
|
| 839 |
+
|
| 840 |
+
|
| 841 |
+
def test_unicode_format():
|
| 842 |
+
df = DataFrame({"A": [1.23, 4.56]})
|
| 843 |
+
result = df.to_csv(float_format="{:.2f}€", encoding="utf-8", lineterminator="\n")
|
| 844 |
+
expected = ",A\n0,1.23€\n1,4.56€\n"
|
| 845 |
+
assert result == expected
|
| 846 |
+
|
| 847 |
+
|
| 848 |
+
def test_empty_dataframe():
|
| 849 |
+
df = DataFrame({"A": []})
|
| 850 |
+
result = df.to_csv(float_format="{:.2f}", lineterminator="\n")
|
| 851 |
+
expected = ",A\n"
|
| 852 |
+
assert result == expected
|
| 853 |
+
|
| 854 |
+
|
| 855 |
+
def test_multi_column_float():
|
| 856 |
+
df = DataFrame({"A": [1.23, 4.56], "B": [7.89, 0.12]})
|
| 857 |
+
result = df.to_csv(float_format="{:.2f}", lineterminator="\n")
|
| 858 |
+
expected = ",A,B\n0,1.23,7.89\n1,4.56,0.12\n"
|
| 859 |
+
assert result == expected
|
| 860 |
+
|
| 861 |
+
|
| 862 |
+
def test_invalid_float_format_type():
|
| 863 |
+
df = DataFrame({"A": [1.23]})
|
| 864 |
+
with pytest.raises(ValueError, match="float_format must be a string or callable"):
|
| 865 |
+
df.to_csv(float_format=123)
|
| 866 |
+
|
| 867 |
+
|
| 868 |
+
def test_new_style_with_inf():
|
| 869 |
+
df = DataFrame({"A": [1.23, np.inf, -np.inf]})
|
| 870 |
+
result = df.to_csv(float_format="{:.2f}", na_rep="NA", lineterminator="\n")
|
| 871 |
+
expected = ",A\n0,1.23\n1,inf\n2,-inf\n"
|
| 872 |
+
assert result == expected
|
| 873 |
+
|
| 874 |
+
|
| 875 |
+
def test_new_style_with_precision_edge():
|
| 876 |
+
df = DataFrame({"A": [1.23456789]})
|
| 877 |
+
result = df.to_csv(float_format="{:.10f}", lineterminator="\n")
|
| 878 |
+
expected = ",A\n0,1.2345678900\n"
|
| 879 |
+
assert result == expected
|
| 880 |
+
|
| 881 |
+
|
| 882 |
+
def test_new_style_with_template():
|
| 883 |
+
df = DataFrame({"A": [1234.56789]})
|
| 884 |
+
result = df.to_csv(float_format="Value: {:,.2f}", lineterminator="\n")
|
| 885 |
+
expected = ',A\n0,"Value: 1,234.57"\n'
|
| 886 |
+
assert result == expected
|
.venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_excel.py
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests formatting as writer-agnostic ExcelCells
|
| 2 |
+
|
| 3 |
+
ExcelFormatter is tested implicitly in pandas/tests/io/excel
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import string
|
| 7 |
+
|
| 8 |
+
import pytest
|
| 9 |
+
|
| 10 |
+
from pandas.errors import CSSWarning
|
| 11 |
+
|
| 12 |
+
import pandas._testing as tm
|
| 13 |
+
|
| 14 |
+
from pandas.io.formats.excel import (
|
| 15 |
+
CssExcelCell,
|
| 16 |
+
CSSToExcelConverter,
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@pytest.mark.parametrize(
|
| 21 |
+
"css,expected",
|
| 22 |
+
[
|
| 23 |
+
# FONT
|
| 24 |
+
# - name
|
| 25 |
+
("font-family: foo,bar", {"font": {"name": "foo"}}),
|
| 26 |
+
('font-family: "foo bar",baz', {"font": {"name": "foo bar"}}),
|
| 27 |
+
("font-family: foo,\nbar", {"font": {"name": "foo"}}),
|
| 28 |
+
("font-family: foo, bar, baz", {"font": {"name": "foo"}}),
|
| 29 |
+
("font-family: bar, foo", {"font": {"name": "bar"}}),
|
| 30 |
+
("font-family: 'foo bar', baz", {"font": {"name": "foo bar"}}),
|
| 31 |
+
("font-family: 'foo \\'bar', baz", {"font": {"name": "foo 'bar"}}),
|
| 32 |
+
('font-family: "foo \\"bar", baz', {"font": {"name": 'foo "bar'}}),
|
| 33 |
+
('font-family: "foo ,bar", baz', {"font": {"name": "foo ,bar"}}),
|
| 34 |
+
# - family
|
| 35 |
+
("font-family: serif", {"font": {"name": "serif", "family": 1}}),
|
| 36 |
+
("font-family: Serif", {"font": {"name": "serif", "family": 1}}),
|
| 37 |
+
("font-family: roman, serif", {"font": {"name": "roman", "family": 1}}),
|
| 38 |
+
("font-family: roman, sans-serif", {"font": {"name": "roman", "family": 2}}),
|
| 39 |
+
("font-family: roman, sans serif", {"font": {"name": "roman"}}),
|
| 40 |
+
("font-family: roman, sansserif", {"font": {"name": "roman"}}),
|
| 41 |
+
("font-family: roman, cursive", {"font": {"name": "roman", "family": 4}}),
|
| 42 |
+
("font-family: roman, fantasy", {"font": {"name": "roman", "family": 5}}),
|
| 43 |
+
# - size
|
| 44 |
+
("font-size: 1em", {"font": {"size": 12}}),
|
| 45 |
+
("font-size: xx-small", {"font": {"size": 6}}),
|
| 46 |
+
("font-size: x-small", {"font": {"size": 7.5}}),
|
| 47 |
+
("font-size: small", {"font": {"size": 9.6}}),
|
| 48 |
+
("font-size: medium", {"font": {"size": 12}}),
|
| 49 |
+
("font-size: large", {"font": {"size": 13.5}}),
|
| 50 |
+
("font-size: x-large", {"font": {"size": 18}}),
|
| 51 |
+
("font-size: xx-large", {"font": {"size": 24}}),
|
| 52 |
+
("font-size: 50%", {"font": {"size": 6}}),
|
| 53 |
+
# - bold
|
| 54 |
+
("font-weight: 100", {"font": {"bold": False}}),
|
| 55 |
+
("font-weight: 200", {"font": {"bold": False}}),
|
| 56 |
+
("font-weight: 300", {"font": {"bold": False}}),
|
| 57 |
+
("font-weight: 400", {"font": {"bold": False}}),
|
| 58 |
+
("font-weight: normal", {"font": {"bold": False}}),
|
| 59 |
+
("font-weight: lighter", {"font": {"bold": False}}),
|
| 60 |
+
("font-weight: bold", {"font": {"bold": True}}),
|
| 61 |
+
("font-weight: bolder", {"font": {"bold": True}}),
|
| 62 |
+
("font-weight: 700", {"font": {"bold": True}}),
|
| 63 |
+
("font-weight: 800", {"font": {"bold": True}}),
|
| 64 |
+
("font-weight: 900", {"font": {"bold": True}}),
|
| 65 |
+
# - italic
|
| 66 |
+
("font-style: italic", {"font": {"italic": True}}),
|
| 67 |
+
("font-style: oblique", {"font": {"italic": True}}),
|
| 68 |
+
# - underline
|
| 69 |
+
("text-decoration: underline", {"font": {"underline": "single"}}),
|
| 70 |
+
("text-decoration: overline", {}),
|
| 71 |
+
("text-decoration: none", {}),
|
| 72 |
+
# - strike
|
| 73 |
+
("text-decoration: line-through", {"font": {"strike": True}}),
|
| 74 |
+
(
|
| 75 |
+
"text-decoration: underline line-through",
|
| 76 |
+
{"font": {"strike": True, "underline": "single"}},
|
| 77 |
+
),
|
| 78 |
+
(
|
| 79 |
+
"text-decoration: underline; text-decoration: line-through",
|
| 80 |
+
{"font": {"strike": True}},
|
| 81 |
+
),
|
| 82 |
+
# - color
|
| 83 |
+
("color: red", {"font": {"color": "FF0000"}}),
|
| 84 |
+
("color: #ff0000", {"font": {"color": "FF0000"}}),
|
| 85 |
+
("color: #f0a", {"font": {"color": "FF00AA"}}),
|
| 86 |
+
# - shadow
|
| 87 |
+
("text-shadow: none", {"font": {"shadow": False}}),
|
| 88 |
+
("text-shadow: 0px -0em 0px #CCC", {"font": {"shadow": False}}),
|
| 89 |
+
("text-shadow: 0px -0em 0px #999", {"font": {"shadow": False}}),
|
| 90 |
+
("text-shadow: 0px -0em 0px", {"font": {"shadow": False}}),
|
| 91 |
+
("text-shadow: 2px -0em 0px #CCC", {"font": {"shadow": True}}),
|
| 92 |
+
("text-shadow: 0px -2em 0px #CCC", {"font": {"shadow": True}}),
|
| 93 |
+
("text-shadow: 0px -0em 2px #CCC", {"font": {"shadow": True}}),
|
| 94 |
+
("text-shadow: 0px -0em 2px", {"font": {"shadow": True}}),
|
| 95 |
+
("text-shadow: 0px -2em", {"font": {"shadow": True}}),
|
| 96 |
+
# FILL
|
| 97 |
+
# - color, fillType
|
| 98 |
+
(
|
| 99 |
+
"background-color: red",
|
| 100 |
+
{"fill": {"fgColor": "FF0000", "patternType": "solid"}},
|
| 101 |
+
),
|
| 102 |
+
(
|
| 103 |
+
"background-color: #ff0000",
|
| 104 |
+
{"fill": {"fgColor": "FF0000", "patternType": "solid"}},
|
| 105 |
+
),
|
| 106 |
+
(
|
| 107 |
+
"background-color: #f0a",
|
| 108 |
+
{"fill": {"fgColor": "FF00AA", "patternType": "solid"}},
|
| 109 |
+
),
|
| 110 |
+
# BORDER
|
| 111 |
+
# - style
|
| 112 |
+
(
|
| 113 |
+
"border-style: solid",
|
| 114 |
+
{
|
| 115 |
+
"border": {
|
| 116 |
+
"top": {"style": "medium"},
|
| 117 |
+
"bottom": {"style": "medium"},
|
| 118 |
+
"left": {"style": "medium"},
|
| 119 |
+
"right": {"style": "medium"},
|
| 120 |
+
}
|
| 121 |
+
},
|
| 122 |
+
),
|
| 123 |
+
(
|
| 124 |
+
"border-style: solid; border-width: thin",
|
| 125 |
+
{
|
| 126 |
+
"border": {
|
| 127 |
+
"top": {"style": "thin"},
|
| 128 |
+
"bottom": {"style": "thin"},
|
| 129 |
+
"left": {"style": "thin"},
|
| 130 |
+
"right": {"style": "thin"},
|
| 131 |
+
}
|
| 132 |
+
},
|
| 133 |
+
),
|
| 134 |
+
(
|
| 135 |
+
"border-top-style: solid; border-top-width: thin",
|
| 136 |
+
{"border": {"top": {"style": "thin"}}},
|
| 137 |
+
),
|
| 138 |
+
(
|
| 139 |
+
"border-top-style: solid; border-top-width: 1pt",
|
| 140 |
+
{"border": {"top": {"style": "thin"}}},
|
| 141 |
+
),
|
| 142 |
+
("border-top-style: solid", {"border": {"top": {"style": "medium"}}}),
|
| 143 |
+
(
|
| 144 |
+
"border-top-style: solid; border-top-width: medium",
|
| 145 |
+
{"border": {"top": {"style": "medium"}}},
|
| 146 |
+
),
|
| 147 |
+
(
|
| 148 |
+
"border-top-style: solid; border-top-width: 2pt",
|
| 149 |
+
{"border": {"top": {"style": "medium"}}},
|
| 150 |
+
),
|
| 151 |
+
(
|
| 152 |
+
"border-top-style: solid; border-top-width: thick",
|
| 153 |
+
{"border": {"top": {"style": "thick"}}},
|
| 154 |
+
),
|
| 155 |
+
(
|
| 156 |
+
"border-top-style: solid; border-top-width: 4pt",
|
| 157 |
+
{"border": {"top": {"style": "thick"}}},
|
| 158 |
+
),
|
| 159 |
+
(
|
| 160 |
+
"border-top-style: solid; border-top-width: none",
|
| 161 |
+
{"border": {"top": {"style": "none"}}},
|
| 162 |
+
),
|
| 163 |
+
(
|
| 164 |
+
"border-top-style: solid; border-top-width: 0.000001pt",
|
| 165 |
+
{"border": {"top": {"style": "none"}}},
|
| 166 |
+
),
|
| 167 |
+
(
|
| 168 |
+
"border-top-style: dotted",
|
| 169 |
+
{"border": {"top": {"style": "mediumDashDotDot"}}},
|
| 170 |
+
),
|
| 171 |
+
(
|
| 172 |
+
"border-top-style: dotted; border-top-width: thin",
|
| 173 |
+
{"border": {"top": {"style": "dotted"}}},
|
| 174 |
+
),
|
| 175 |
+
("border-top-style: dashed", {"border": {"top": {"style": "mediumDashed"}}}),
|
| 176 |
+
(
|
| 177 |
+
"border-top-style: dashed; border-top-width: thin",
|
| 178 |
+
{"border": {"top": {"style": "dashed"}}},
|
| 179 |
+
),
|
| 180 |
+
("border-top-style: double", {"border": {"top": {"style": "double"}}}),
|
| 181 |
+
# - color
|
| 182 |
+
(
|
| 183 |
+
"border-style: solid; border-color: #0000ff",
|
| 184 |
+
{
|
| 185 |
+
"border": {
|
| 186 |
+
"top": {"style": "medium", "color": "0000FF"},
|
| 187 |
+
"right": {"style": "medium", "color": "0000FF"},
|
| 188 |
+
"bottom": {"style": "medium", "color": "0000FF"},
|
| 189 |
+
"left": {"style": "medium", "color": "0000FF"},
|
| 190 |
+
}
|
| 191 |
+
},
|
| 192 |
+
),
|
| 193 |
+
(
|
| 194 |
+
"border-top-style: double; border-top-color: blue",
|
| 195 |
+
{"border": {"top": {"style": "double", "color": "0000FF"}}},
|
| 196 |
+
),
|
| 197 |
+
(
|
| 198 |
+
"border-top-style: solid; border-top-color: #06c",
|
| 199 |
+
{"border": {"top": {"style": "medium", "color": "0066CC"}}},
|
| 200 |
+
),
|
| 201 |
+
(
|
| 202 |
+
"border-top-color: blue",
|
| 203 |
+
{"border": {"top": {"color": "0000FF", "style": "none"}}},
|
| 204 |
+
),
|
| 205 |
+
(
|
| 206 |
+
"border-top-style: slantDashDot; border-top-color: blue",
|
| 207 |
+
{"border": {"top": {"style": "slantDashDot", "color": "0000FF"}}},
|
| 208 |
+
),
|
| 209 |
+
# ALIGNMENT
|
| 210 |
+
# - horizontal
|
| 211 |
+
("text-align: center", {"alignment": {"horizontal": "center"}}),
|
| 212 |
+
("text-align: left", {"alignment": {"horizontal": "left"}}),
|
| 213 |
+
("text-align: right", {"alignment": {"horizontal": "right"}}),
|
| 214 |
+
("text-align: justify", {"alignment": {"horizontal": "justify"}}),
|
| 215 |
+
# - vertical
|
| 216 |
+
("vertical-align: top", {"alignment": {"vertical": "top"}}),
|
| 217 |
+
("vertical-align: text-top", {"alignment": {"vertical": "top"}}),
|
| 218 |
+
("vertical-align: middle", {"alignment": {"vertical": "center"}}),
|
| 219 |
+
("vertical-align: bottom", {"alignment": {"vertical": "bottom"}}),
|
| 220 |
+
("vertical-align: text-bottom", {"alignment": {"vertical": "bottom"}}),
|
| 221 |
+
# - wrap_text
|
| 222 |
+
("white-space: nowrap", {"alignment": {"wrap_text": False}}),
|
| 223 |
+
("white-space: pre", {"alignment": {"wrap_text": False}}),
|
| 224 |
+
("white-space: pre-line", {"alignment": {"wrap_text": False}}),
|
| 225 |
+
("white-space: normal", {"alignment": {"wrap_text": True}}),
|
| 226 |
+
# NUMBER FORMAT
|
| 227 |
+
("number-format: 0%", {"number_format": {"format_code": "0%"}}),
|
| 228 |
+
(
|
| 229 |
+
"number-format: 0§[Red](0)§-§@;",
|
| 230 |
+
{"number_format": {"format_code": "0;[red](0);-;@"}}, # GH 46152
|
| 231 |
+
),
|
| 232 |
+
],
|
| 233 |
+
)
|
| 234 |
+
def test_css_to_excel(css, expected):
|
| 235 |
+
convert = CSSToExcelConverter()
|
| 236 |
+
assert expected == convert(css)
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
def test_css_to_excel_multiple():
|
| 240 |
+
convert = CSSToExcelConverter()
|
| 241 |
+
actual = convert(
|
| 242 |
+
"""
|
| 243 |
+
font-weight: bold;
|
| 244 |
+
text-decoration: underline;
|
| 245 |
+
color: red;
|
| 246 |
+
border-width: thin;
|
| 247 |
+
text-align: center;
|
| 248 |
+
vertical-align: top;
|
| 249 |
+
unused: something;
|
| 250 |
+
"""
|
| 251 |
+
)
|
| 252 |
+
assert {
|
| 253 |
+
"font": {"bold": True, "underline": "single", "color": "FF0000"},
|
| 254 |
+
"border": {
|
| 255 |
+
"top": {"style": "thin"},
|
| 256 |
+
"right": {"style": "thin"},
|
| 257 |
+
"bottom": {"style": "thin"},
|
| 258 |
+
"left": {"style": "thin"},
|
| 259 |
+
},
|
| 260 |
+
"alignment": {"horizontal": "center", "vertical": "top"},
|
| 261 |
+
} == actual
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
@pytest.mark.parametrize(
|
| 265 |
+
"css",
|
| 266 |
+
[
|
| 267 |
+
"border-top-style: unhandled-border-style",
|
| 268 |
+
"border-style: another-unhandled-style",
|
| 269 |
+
],
|
| 270 |
+
)
|
| 271 |
+
def test_css_to_excel_unhandled_border_style_warns(css):
|
| 272 |
+
"""Test that unhandled border styles raise a CSSWarning."""
|
| 273 |
+
convert = CSSToExcelConverter()
|
| 274 |
+
with tm.assert_produces_warning(CSSWarning, match="Unhandled border style format"):
|
| 275 |
+
convert(css)
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
@pytest.mark.parametrize(
|
| 279 |
+
"css,inherited,expected",
|
| 280 |
+
[
|
| 281 |
+
("font-weight: bold", "", {"font": {"bold": True}}),
|
| 282 |
+
("", "font-weight: bold", {"font": {"bold": True}}),
|
| 283 |
+
(
|
| 284 |
+
"font-weight: bold",
|
| 285 |
+
"font-style: italic",
|
| 286 |
+
{"font": {"bold": True, "italic": True}},
|
| 287 |
+
),
|
| 288 |
+
("font-style: normal", "font-style: italic", {"font": {"italic": False}}),
|
| 289 |
+
("font-style: inherit", "", {}),
|
| 290 |
+
(
|
| 291 |
+
"font-style: normal; font-style: inherit",
|
| 292 |
+
"font-style: italic",
|
| 293 |
+
{"font": {"italic": True}},
|
| 294 |
+
),
|
| 295 |
+
],
|
| 296 |
+
)
|
| 297 |
+
def test_css_to_excel_inherited(css, inherited, expected):
|
| 298 |
+
convert = CSSToExcelConverter(inherited)
|
| 299 |
+
assert expected == convert(css)
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
@pytest.mark.parametrize(
|
| 303 |
+
"input_color,output_color",
|
| 304 |
+
(
|
| 305 |
+
list(CSSToExcelConverter.NAMED_COLORS.items())
|
| 306 |
+
+ [("#" + rgb, rgb) for rgb in CSSToExcelConverter.NAMED_COLORS.values()]
|
| 307 |
+
+ [("#F0F", "FF00FF"), ("#ABC", "AABBCC")]
|
| 308 |
+
),
|
| 309 |
+
)
|
| 310 |
+
def test_css_to_excel_good_colors(input_color, output_color):
|
| 311 |
+
# see gh-18392
|
| 312 |
+
css = (
|
| 313 |
+
f"border-top-color: {input_color}; "
|
| 314 |
+
f"border-right-color: {input_color}; "
|
| 315 |
+
f"border-bottom-color: {input_color}; "
|
| 316 |
+
f"border-left-color: {input_color}; "
|
| 317 |
+
f"background-color: {input_color}; "
|
| 318 |
+
f"color: {input_color}"
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
expected = {}
|
| 322 |
+
|
| 323 |
+
expected["fill"] = {"patternType": "solid", "fgColor": output_color}
|
| 324 |
+
|
| 325 |
+
expected["font"] = {"color": output_color}
|
| 326 |
+
|
| 327 |
+
expected["border"] = {
|
| 328 |
+
k: {"color": output_color, "style": "none"}
|
| 329 |
+
for k in ("top", "right", "bottom", "left")
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
with tm.assert_produces_warning(None):
|
| 333 |
+
convert = CSSToExcelConverter()
|
| 334 |
+
assert expected == convert(css)
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
@pytest.mark.parametrize("input_color", [None, "not-a-color"])
|
| 338 |
+
def test_css_to_excel_bad_colors(input_color):
|
| 339 |
+
# see gh-18392
|
| 340 |
+
css = (
|
| 341 |
+
f"border-top-color: {input_color}; "
|
| 342 |
+
f"border-right-color: {input_color}; "
|
| 343 |
+
f"border-bottom-color: {input_color}; "
|
| 344 |
+
f"border-left-color: {input_color}; "
|
| 345 |
+
f"background-color: {input_color}; "
|
| 346 |
+
f"color: {input_color}"
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
expected = {}
|
| 350 |
+
|
| 351 |
+
if input_color is not None:
|
| 352 |
+
expected["fill"] = {"patternType": "solid"}
|
| 353 |
+
|
| 354 |
+
with tm.assert_produces_warning(CSSWarning, match="Unhandled color format"):
|
| 355 |
+
convert = CSSToExcelConverter()
|
| 356 |
+
assert expected == convert(css)
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
@pytest.mark.parametrize("input_color", ["#", "#1234567"])
|
| 360 |
+
def test_css_to_excel_invalid_color_raises(input_color):
|
| 361 |
+
"""Test that invalid colors raise a ValueError."""
|
| 362 |
+
css = (
|
| 363 |
+
f"border-top-color: {input_color}; "
|
| 364 |
+
f"border-right-color: {input_color}; "
|
| 365 |
+
f"border-bottom-color: {input_color}; "
|
| 366 |
+
f"border-left-color: {input_color}; "
|
| 367 |
+
f"background-color: {input_color}; "
|
| 368 |
+
f"color: {input_color}"
|
| 369 |
+
)
|
| 370 |
+
|
| 371 |
+
convert = CSSToExcelConverter()
|
| 372 |
+
with pytest.raises(ValueError, match=f"Unexpected color {input_color}"):
|
| 373 |
+
convert(css)
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
def tests_css_named_colors_valid():
|
| 377 |
+
upper_hexs = set(map(str.upper, string.hexdigits))
|
| 378 |
+
for color in CSSToExcelConverter.NAMED_COLORS.values():
|
| 379 |
+
assert len(color) == 6 and all(c in upper_hexs for c in color)
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
def test_css_named_colors_from_mpl_present():
|
| 383 |
+
mpl_colors = pytest.importorskip("matplotlib.colors")
|
| 384 |
+
|
| 385 |
+
pd_colors = CSSToExcelConverter.NAMED_COLORS
|
| 386 |
+
for name, color in mpl_colors.CSS4_COLORS.items():
|
| 387 |
+
assert name in pd_colors and pd_colors[name] == color[1:]
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
@pytest.mark.parametrize(
|
| 391 |
+
"styles,expected",
|
| 392 |
+
[
|
| 393 |
+
([("color", "green"), ("color", "red")], "color: red;"),
|
| 394 |
+
([("font-weight", "bold"), ("font-weight", "normal")], "font-weight: normal;"),
|
| 395 |
+
([("text-align", "center"), ("TEXT-ALIGN", "right")], "text-align: right;"),
|
| 396 |
+
],
|
| 397 |
+
)
|
| 398 |
+
def test_css_excel_cell_precedence(styles, expected):
|
| 399 |
+
"""It applies favors latter declarations over former declarations"""
|
| 400 |
+
# See GH 47371
|
| 401 |
+
converter = CSSToExcelConverter()
|
| 402 |
+
converter._call_cached.cache_clear()
|
| 403 |
+
css_styles = {(0, 0): styles}
|
| 404 |
+
cell = CssExcelCell(
|
| 405 |
+
row=0,
|
| 406 |
+
col=0,
|
| 407 |
+
val="",
|
| 408 |
+
style=None,
|
| 409 |
+
css_styles=css_styles,
|
| 410 |
+
css_row=0,
|
| 411 |
+
css_col=0,
|
| 412 |
+
css_converter=converter,
|
| 413 |
+
)
|
| 414 |
+
converter._call_cached.cache_clear()
|
| 415 |
+
|
| 416 |
+
assert cell.style == converter(expected)
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
@pytest.mark.parametrize(
|
| 420 |
+
"styles,cache_hits,cache_misses",
|
| 421 |
+
[
|
| 422 |
+
([[("color", "green"), ("color", "red"), ("color", "green")]], 0, 1),
|
| 423 |
+
(
|
| 424 |
+
[
|
| 425 |
+
[("font-weight", "bold")],
|
| 426 |
+
[("font-weight", "normal"), ("font-weight", "bold")],
|
| 427 |
+
],
|
| 428 |
+
1,
|
| 429 |
+
1,
|
| 430 |
+
),
|
| 431 |
+
([[("text-align", "center")], [("TEXT-ALIGN", "center")]], 1, 1),
|
| 432 |
+
(
|
| 433 |
+
[
|
| 434 |
+
[("font-weight", "bold"), ("text-align", "center")],
|
| 435 |
+
[("font-weight", "bold"), ("text-align", "left")],
|
| 436 |
+
],
|
| 437 |
+
0,
|
| 438 |
+
2,
|
| 439 |
+
),
|
| 440 |
+
(
|
| 441 |
+
[
|
| 442 |
+
[("font-weight", "bold"), ("text-align", "center")],
|
| 443 |
+
[("font-weight", "bold"), ("text-align", "left")],
|
| 444 |
+
[("font-weight", "bold"), ("text-align", "center")],
|
| 445 |
+
],
|
| 446 |
+
1,
|
| 447 |
+
2,
|
| 448 |
+
),
|
| 449 |
+
],
|
| 450 |
+
)
|
| 451 |
+
def test_css_excel_cell_cache(styles, cache_hits, cache_misses):
|
| 452 |
+
"""It caches unique cell styles"""
|
| 453 |
+
# See GH 47371
|
| 454 |
+
converter = CSSToExcelConverter()
|
| 455 |
+
converter._call_cached.cache_clear()
|
| 456 |
+
|
| 457 |
+
css_styles = {(0, i): _style for i, _style in enumerate(styles)}
|
| 458 |
+
for css_row, css_col in css_styles:
|
| 459 |
+
CssExcelCell(
|
| 460 |
+
row=0,
|
| 461 |
+
col=0,
|
| 462 |
+
val="",
|
| 463 |
+
style=None,
|
| 464 |
+
css_styles=css_styles,
|
| 465 |
+
css_row=css_row,
|
| 466 |
+
css_col=css_col,
|
| 467 |
+
css_converter=converter,
|
| 468 |
+
)
|
| 469 |
+
cache_info = converter._call_cached.cache_info()
|
| 470 |
+
converter._call_cached.cache_clear()
|
| 471 |
+
|
| 472 |
+
assert cache_info.hits == cache_hits
|
| 473 |
+
assert cache_info.misses == cache_misses
|
.venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_html.py
ADDED
|
@@ -0,0 +1,1166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
from io import StringIO
|
| 3 |
+
import itertools
|
| 4 |
+
import re
|
| 5 |
+
import textwrap
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import pytest
|
| 9 |
+
|
| 10 |
+
import pandas as pd
|
| 11 |
+
from pandas import (
|
| 12 |
+
DataFrame,
|
| 13 |
+
Index,
|
| 14 |
+
MultiIndex,
|
| 15 |
+
get_option,
|
| 16 |
+
option_context,
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
import pandas.io.formats.format as fmt
|
| 20 |
+
|
| 21 |
+
lorem_ipsum = (
|
| 22 |
+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod "
|
| 23 |
+
"tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim "
|
| 24 |
+
"veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex "
|
| 25 |
+
"ea commodo consequat. Duis aute irure dolor in reprehenderit in "
|
| 26 |
+
"voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur "
|
| 27 |
+
"sint occaecat cupidatat non proident, sunt in culpa qui officia "
|
| 28 |
+
"deserunt mollit anim id est laborum."
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def expected_html(datapath, name):
|
| 33 |
+
"""
|
| 34 |
+
Read HTML file from formats data directory.
|
| 35 |
+
|
| 36 |
+
Parameters
|
| 37 |
+
----------
|
| 38 |
+
datapath : pytest fixture
|
| 39 |
+
The datapath fixture injected into a test by pytest.
|
| 40 |
+
name : str
|
| 41 |
+
The name of the HTML file without the suffix.
|
| 42 |
+
|
| 43 |
+
Returns
|
| 44 |
+
-------
|
| 45 |
+
str : contents of HTML file.
|
| 46 |
+
"""
|
| 47 |
+
filename = ".".join([name, "html"])
|
| 48 |
+
filepath = datapath("io", "formats", "data", "html", filename)
|
| 49 |
+
with open(filepath, encoding="utf-8") as f:
|
| 50 |
+
html = f.read()
|
| 51 |
+
return html.rstrip()
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
@pytest.fixture(params=["mixed", "empty"])
|
| 55 |
+
def biggie_df_fixture(request):
|
| 56 |
+
"""Fixture for a big mixed Dataframe and an empty Dataframe"""
|
| 57 |
+
if request.param == "mixed":
|
| 58 |
+
df = DataFrame(
|
| 59 |
+
{
|
| 60 |
+
"A": np.random.default_rng(2).standard_normal(200),
|
| 61 |
+
"B": Index([f"{i}?!" for i in range(200)]),
|
| 62 |
+
},
|
| 63 |
+
index=np.arange(200),
|
| 64 |
+
)
|
| 65 |
+
df.loc[:20, "A"] = np.nan
|
| 66 |
+
df.loc[:20, "B"] = np.nan
|
| 67 |
+
return df
|
| 68 |
+
elif request.param == "empty":
|
| 69 |
+
df = DataFrame(index=np.arange(200))
|
| 70 |
+
return df
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@pytest.fixture(params=fmt.VALID_JUSTIFY_PARAMETERS)
|
| 74 |
+
def justify(request):
|
| 75 |
+
return request.param
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
@pytest.mark.parametrize("col_space", [30, 50])
|
| 79 |
+
def test_to_html_with_col_space(col_space):
|
| 80 |
+
df = DataFrame(np.random.default_rng(2).random(size=(1, 3)))
|
| 81 |
+
# check that col_space affects HTML generation
|
| 82 |
+
# and be very brittle about it.
|
| 83 |
+
result = df.to_html(col_space=col_space)
|
| 84 |
+
hdrs = [x for x in result.split(r"\n") if re.search(r"<th[>\s]", x)]
|
| 85 |
+
assert len(hdrs) > 0
|
| 86 |
+
for h in hdrs:
|
| 87 |
+
assert "min-width" in h
|
| 88 |
+
assert str(col_space) in h
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def test_to_html_with_column_specific_col_space_raises():
|
| 92 |
+
df = DataFrame(
|
| 93 |
+
np.random.default_rng(2).random(size=(3, 3)), columns=["a", "b", "c"]
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
msg = (
|
| 97 |
+
"Col_space length\\(\\d+\\) should match DataFrame number of columns\\(\\d+\\)"
|
| 98 |
+
)
|
| 99 |
+
with pytest.raises(ValueError, match=msg):
|
| 100 |
+
df.to_html(col_space=[30, 40])
|
| 101 |
+
|
| 102 |
+
with pytest.raises(ValueError, match=msg):
|
| 103 |
+
df.to_html(col_space=[30, 40, 50, 60])
|
| 104 |
+
|
| 105 |
+
msg = "unknown column"
|
| 106 |
+
with pytest.raises(ValueError, match=msg):
|
| 107 |
+
df.to_html(col_space={"a": "foo", "b": 23, "d": 34})
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def test_to_html_with_column_specific_col_space():
|
| 111 |
+
df = DataFrame(
|
| 112 |
+
np.random.default_rng(2).random(size=(3, 3)), columns=["a", "b", "c"]
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
result = df.to_html(col_space={"a": "2em", "b": 23})
|
| 116 |
+
hdrs = [x for x in result.split("\n") if re.search(r"<th[>\s]", x)]
|
| 117 |
+
assert 'min-width: 2em;">a</th>' in hdrs[1]
|
| 118 |
+
assert 'min-width: 23px;">b</th>' in hdrs[2]
|
| 119 |
+
assert "<th>c</th>" in hdrs[3]
|
| 120 |
+
|
| 121 |
+
result = df.to_html(col_space=["1em", 2, 3])
|
| 122 |
+
hdrs = [x for x in result.split("\n") if re.search(r"<th[>\s]", x)]
|
| 123 |
+
assert 'min-width: 1em;">a</th>' in hdrs[1]
|
| 124 |
+
assert 'min-width: 2px;">b</th>' in hdrs[2]
|
| 125 |
+
assert 'min-width: 3px;">c</th>' in hdrs[3]
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def test_to_html_with_empty_string_label():
|
| 129 |
+
# GH 3547, to_html regards empty string labels as repeated labels
|
| 130 |
+
data = {"c1": ["a", "b"], "c2": ["a", ""], "data": [1, 2]}
|
| 131 |
+
df = DataFrame(data).set_index(["c1", "c2"])
|
| 132 |
+
result = df.to_html()
|
| 133 |
+
assert "rowspan" not in result
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
@pytest.mark.parametrize(
|
| 137 |
+
"df_data,expected",
|
| 138 |
+
[
|
| 139 |
+
({"\u03c3": np.arange(10.0)}, "unicode_1"),
|
| 140 |
+
({"A": ["\u03c3"]}, "unicode_2"),
|
| 141 |
+
],
|
| 142 |
+
)
|
| 143 |
+
def test_to_html_unicode(df_data, expected, datapath):
|
| 144 |
+
df = DataFrame(df_data)
|
| 145 |
+
expected = expected_html(datapath, expected)
|
| 146 |
+
result = df.to_html()
|
| 147 |
+
assert result == expected
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def test_to_html_encoding(float_frame, temp_file):
|
| 151 |
+
# GH 28663
|
| 152 |
+
float_frame.to_html(temp_file, encoding="gbk")
|
| 153 |
+
expected = temp_file.read_text(encoding="gbk")
|
| 154 |
+
assert float_frame.to_html() == expected
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def test_to_html_decimal(datapath):
|
| 158 |
+
# GH 12031
|
| 159 |
+
df = DataFrame({"A": [6.0, 3.1, 2.2]})
|
| 160 |
+
result = df.to_html(decimal=",")
|
| 161 |
+
expected = expected_html(datapath, "gh12031_expected_output")
|
| 162 |
+
assert result == expected
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
@pytest.mark.parametrize(
|
| 166 |
+
"kwargs,string,expected",
|
| 167 |
+
[
|
| 168 |
+
({}, "<type 'str'>", "escaped"),
|
| 169 |
+
({"escape": False}, "<b>bold</b>", "escape_disabled"),
|
| 170 |
+
],
|
| 171 |
+
)
|
| 172 |
+
def test_to_html_escaped(kwargs, string, expected, datapath):
|
| 173 |
+
a = "str<ing1 &"
|
| 174 |
+
b = "stri>ng2 &"
|
| 175 |
+
|
| 176 |
+
test_dict = {"co<l1": {a: string, b: string}, "co>l2": {a: string, b: string}}
|
| 177 |
+
result = DataFrame(test_dict).to_html(**kwargs)
|
| 178 |
+
expected = expected_html(datapath, expected)
|
| 179 |
+
assert result == expected
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
@pytest.mark.parametrize("index_is_named", [True, False])
|
| 183 |
+
def test_to_html_multiindex_index_false(index_is_named, datapath):
|
| 184 |
+
# GH 8452
|
| 185 |
+
df = DataFrame(
|
| 186 |
+
{"a": range(2), "b": range(3, 5), "c": range(5, 7), "d": range(3, 5)}
|
| 187 |
+
)
|
| 188 |
+
df.columns = MultiIndex.from_product([["a", "b"], ["c", "d"]])
|
| 189 |
+
if index_is_named:
|
| 190 |
+
df.index = Index(df.index.values, name="idx")
|
| 191 |
+
result = df.to_html(index=False)
|
| 192 |
+
expected = expected_html(datapath, "gh8452_expected_output")
|
| 193 |
+
assert result == expected
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
@pytest.mark.parametrize(
|
| 197 |
+
"multi_sparse,expected",
|
| 198 |
+
[
|
| 199 |
+
(False, "multiindex_sparsify_false_multi_sparse_1"),
|
| 200 |
+
(False, "multiindex_sparsify_false_multi_sparse_2"),
|
| 201 |
+
(True, "multiindex_sparsify_1"),
|
| 202 |
+
(True, "multiindex_sparsify_2"),
|
| 203 |
+
],
|
| 204 |
+
)
|
| 205 |
+
def test_to_html_multiindex_sparsify(multi_sparse, expected, datapath):
|
| 206 |
+
index = MultiIndex.from_arrays([[0, 0, 1, 1], [0, 1, 0, 1]], names=["foo", None])
|
| 207 |
+
df = DataFrame([[0, 1], [2, 3], [4, 5], [6, 7]], index=index)
|
| 208 |
+
if expected.endswith("2"):
|
| 209 |
+
df.columns = index[::2]
|
| 210 |
+
with option_context("display.multi_sparse", multi_sparse):
|
| 211 |
+
result = df.to_html()
|
| 212 |
+
expected = expected_html(datapath, expected)
|
| 213 |
+
assert result == expected
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
@pytest.mark.parametrize(
|
| 217 |
+
"max_rows,expected",
|
| 218 |
+
[
|
| 219 |
+
(60, "gh14882_expected_output_1"),
|
| 220 |
+
# Test that ... appears in a middle level
|
| 221 |
+
(56, "gh14882_expected_output_2"),
|
| 222 |
+
],
|
| 223 |
+
)
|
| 224 |
+
def test_to_html_multiindex_odd_even_truncate(max_rows, expected, datapath):
|
| 225 |
+
# GH 14882 - Issue on truncation with odd length DataFrame
|
| 226 |
+
index = MultiIndex.from_product(
|
| 227 |
+
[[100, 200, 300], [10, 20, 30], [1, 2, 3, 4, 5, 6, 7]], names=["a", "b", "c"]
|
| 228 |
+
)
|
| 229 |
+
df = DataFrame({"n": range(len(index))}, index=index)
|
| 230 |
+
result = df.to_html(max_rows=max_rows)
|
| 231 |
+
expected = expected_html(datapath, expected)
|
| 232 |
+
assert result == expected
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
@pytest.mark.parametrize(
|
| 236 |
+
"df,formatters,expected",
|
| 237 |
+
[
|
| 238 |
+
(
|
| 239 |
+
DataFrame(
|
| 240 |
+
[[0, 1], [2, 3], [4, 5], [6, 7]],
|
| 241 |
+
columns=Index(["foo", None], dtype=object),
|
| 242 |
+
index=np.arange(4),
|
| 243 |
+
),
|
| 244 |
+
{"__index__": lambda x: "abcd"[x]},
|
| 245 |
+
"index_formatter",
|
| 246 |
+
),
|
| 247 |
+
(
|
| 248 |
+
DataFrame({"months": [datetime(2016, 1, 1), datetime(2016, 2, 2)]}),
|
| 249 |
+
{"months": lambda x: x.strftime("%Y-%m")},
|
| 250 |
+
"datetime64_monthformatter",
|
| 251 |
+
),
|
| 252 |
+
(
|
| 253 |
+
DataFrame(
|
| 254 |
+
{
|
| 255 |
+
"hod": pd.to_datetime(
|
| 256 |
+
["10:10:10.100", "12:12:12.120"], format="%H:%M:%S.%f"
|
| 257 |
+
)
|
| 258 |
+
}
|
| 259 |
+
),
|
| 260 |
+
{"hod": lambda x: x.strftime("%H:%M")},
|
| 261 |
+
"datetime64_hourformatter",
|
| 262 |
+
),
|
| 263 |
+
(
|
| 264 |
+
DataFrame(
|
| 265 |
+
{
|
| 266 |
+
"i": pd.Series([1, 2], dtype="int64"),
|
| 267 |
+
"f": pd.Series([1, 2], dtype="float64"),
|
| 268 |
+
"I": pd.Series([1, 2], dtype="Int64"),
|
| 269 |
+
"s": pd.Series([1, 2], dtype="string"),
|
| 270 |
+
"b": pd.Series([True, False], dtype="boolean"),
|
| 271 |
+
"c": pd.Series(["a", "b"], dtype=pd.CategoricalDtype(["a", "b"])),
|
| 272 |
+
"o": pd.Series([1, "2"], dtype=object),
|
| 273 |
+
}
|
| 274 |
+
),
|
| 275 |
+
[lambda x: "formatted"] * 7,
|
| 276 |
+
"various_dtypes_formatted",
|
| 277 |
+
),
|
| 278 |
+
],
|
| 279 |
+
)
|
| 280 |
+
def test_to_html_formatters(df, formatters, expected, datapath):
|
| 281 |
+
expected = expected_html(datapath, expected)
|
| 282 |
+
result = df.to_html(formatters=formatters)
|
| 283 |
+
assert result == expected
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
def test_to_html_regression_GH6098():
|
| 287 |
+
df = DataFrame(
|
| 288 |
+
{
|
| 289 |
+
"clé1": ["a", "a", "b", "b", "a"],
|
| 290 |
+
"clé2": ["1er", "2ème", "1er", "2ème", "1er"],
|
| 291 |
+
"données1": np.random.default_rng(2).standard_normal(5),
|
| 292 |
+
"données2": np.random.default_rng(2).standard_normal(5),
|
| 293 |
+
}
|
| 294 |
+
)
|
| 295 |
+
|
| 296 |
+
# it works
|
| 297 |
+
df.pivot_table(index=["clé1"], columns=["clé2"])._repr_html_()
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
def test_to_html_truncate(datapath):
|
| 301 |
+
index = pd.date_range(start="20010101", freq="D", periods=20)
|
| 302 |
+
df = DataFrame(index=index, columns=range(20))
|
| 303 |
+
result = df.to_html(max_rows=8, max_cols=4)
|
| 304 |
+
expected = expected_html(datapath, "truncate")
|
| 305 |
+
assert result == expected
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
@pytest.mark.parametrize("size", [1, 5])
|
| 309 |
+
def test_html_invalid_formatters_arg_raises(size):
|
| 310 |
+
# issue-28469
|
| 311 |
+
df = DataFrame(columns=["a", "b", "c"])
|
| 312 |
+
msg = "Formatters length({}) should match DataFrame number of columns(3)"
|
| 313 |
+
with pytest.raises(ValueError, match=re.escape(msg.format(size))):
|
| 314 |
+
df.to_html(formatters=["{}".format] * size)
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def test_to_html_truncate_formatter(datapath):
|
| 318 |
+
# issue-25955
|
| 319 |
+
data = [
|
| 320 |
+
{"A": 1, "B": 2, "C": 3, "D": 4},
|
| 321 |
+
{"A": 5, "B": 6, "C": 7, "D": 8},
|
| 322 |
+
{"A": 9, "B": 10, "C": 11, "D": 12},
|
| 323 |
+
{"A": 13, "B": 14, "C": 15, "D": 16},
|
| 324 |
+
]
|
| 325 |
+
|
| 326 |
+
df = DataFrame(data)
|
| 327 |
+
fmt = lambda x: str(x) + "_mod"
|
| 328 |
+
formatters = [fmt, fmt, None, None]
|
| 329 |
+
result = df.to_html(formatters=formatters, max_cols=3)
|
| 330 |
+
expected = expected_html(datapath, "truncate_formatter")
|
| 331 |
+
assert result == expected
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
@pytest.mark.parametrize(
|
| 335 |
+
"sparsify,expected",
|
| 336 |
+
[(True, "truncate_multi_index"), (False, "truncate_multi_index_sparse_off")],
|
| 337 |
+
)
|
| 338 |
+
def test_to_html_truncate_multi_index(sparsify, expected, datapath):
|
| 339 |
+
arrays = [
|
| 340 |
+
["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"],
|
| 341 |
+
["one", "two", "one", "two", "one", "two", "one", "two"],
|
| 342 |
+
]
|
| 343 |
+
df = DataFrame(index=arrays, columns=arrays)
|
| 344 |
+
result = df.to_html(max_rows=7, max_cols=7, sparsify=sparsify)
|
| 345 |
+
expected = expected_html(datapath, expected)
|
| 346 |
+
assert result == expected
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
@pytest.mark.parametrize(
|
| 350 |
+
"option,result,expected",
|
| 351 |
+
[
|
| 352 |
+
(None, lambda df: df.to_html(), "1"),
|
| 353 |
+
(None, lambda df: df.to_html(border=2), "2"),
|
| 354 |
+
(2, lambda df: df.to_html(), "2"),
|
| 355 |
+
(2, lambda df: df._repr_html_(), "2"),
|
| 356 |
+
],
|
| 357 |
+
)
|
| 358 |
+
def test_to_html_border(option, result, expected):
|
| 359 |
+
df = DataFrame({"A": [1, 2]})
|
| 360 |
+
if option is None:
|
| 361 |
+
result = result(df)
|
| 362 |
+
else:
|
| 363 |
+
with option_context("display.html.border", option):
|
| 364 |
+
result = result(df)
|
| 365 |
+
expected = f'border="{expected}"'
|
| 366 |
+
assert expected in result
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
@pytest.mark.parametrize("biggie_df_fixture", ["mixed"], indirect=True)
|
| 370 |
+
def test_to_html(biggie_df_fixture):
|
| 371 |
+
# TODO: split this test
|
| 372 |
+
df = biggie_df_fixture
|
| 373 |
+
s = df.to_html()
|
| 374 |
+
|
| 375 |
+
buf = StringIO()
|
| 376 |
+
retval = df.to_html(buf=buf)
|
| 377 |
+
assert retval is None
|
| 378 |
+
assert buf.getvalue() == s
|
| 379 |
+
|
| 380 |
+
assert isinstance(s, str)
|
| 381 |
+
|
| 382 |
+
df.to_html(columns=["B", "A"], col_space=17)
|
| 383 |
+
df.to_html(columns=["B", "A"], formatters={"A": lambda x: f"{x:.1f}"})
|
| 384 |
+
|
| 385 |
+
df.to_html(columns=["B", "A"], float_format=str)
|
| 386 |
+
df.to_html(columns=["B", "A"], col_space=12, float_format=str)
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
@pytest.mark.parametrize("biggie_df_fixture", ["empty"], indirect=True)
|
| 390 |
+
def test_to_html_empty_dataframe(biggie_df_fixture):
|
| 391 |
+
df = biggie_df_fixture
|
| 392 |
+
df.to_html()
|
| 393 |
+
|
| 394 |
+
|
| 395 |
+
def test_to_html_filename(biggie_df_fixture, tmpdir):
|
| 396 |
+
df = biggie_df_fixture
|
| 397 |
+
expected = df.to_html()
|
| 398 |
+
path = tmpdir.join("test.html")
|
| 399 |
+
df.to_html(path)
|
| 400 |
+
result = path.read()
|
| 401 |
+
assert result == expected
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
def test_to_html_with_no_bold():
|
| 405 |
+
df = DataFrame({"x": np.random.default_rng(2).standard_normal(5)})
|
| 406 |
+
html = df.to_html(bold_rows=False)
|
| 407 |
+
result = html[html.find("</thead>")]
|
| 408 |
+
assert "<strong" not in result
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
def test_to_html_columns_arg(float_frame):
|
| 412 |
+
result = float_frame.to_html(columns=["A"])
|
| 413 |
+
assert "<th>B</th>" not in result
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
@pytest.mark.parametrize(
|
| 417 |
+
"columns,justify,expected",
|
| 418 |
+
[
|
| 419 |
+
(
|
| 420 |
+
MultiIndex.from_arrays(
|
| 421 |
+
[np.arange(2).repeat(2), np.mod(range(4), 2)],
|
| 422 |
+
names=["CL0", "CL1"],
|
| 423 |
+
),
|
| 424 |
+
"left",
|
| 425 |
+
"multiindex_1",
|
| 426 |
+
),
|
| 427 |
+
(
|
| 428 |
+
MultiIndex.from_arrays([np.arange(4), np.mod(range(4), 2)]),
|
| 429 |
+
"right",
|
| 430 |
+
"multiindex_2",
|
| 431 |
+
),
|
| 432 |
+
],
|
| 433 |
+
)
|
| 434 |
+
def test_to_html_multiindex(columns, justify, expected, datapath):
|
| 435 |
+
df = DataFrame([list("abcd"), list("efgh")], columns=columns)
|
| 436 |
+
result = df.to_html(justify=justify)
|
| 437 |
+
expected = expected_html(datapath, expected)
|
| 438 |
+
assert result == expected
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
def test_to_html_justify(justify, datapath):
|
| 442 |
+
df = DataFrame(
|
| 443 |
+
{"A": [6, 30000, 2], "B": [1, 2, 70000], "C": [223442, 0, 1]},
|
| 444 |
+
columns=["A", "B", "C"],
|
| 445 |
+
)
|
| 446 |
+
result = df.to_html(justify=justify)
|
| 447 |
+
expected = expected_html(datapath, "justify").format(justify=justify)
|
| 448 |
+
assert result == expected
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
@pytest.mark.parametrize(
|
| 452 |
+
"justify", ["super-right", "small-left", "noinherit", "tiny", "pandas"]
|
| 453 |
+
)
|
| 454 |
+
def test_to_html_invalid_justify(justify):
|
| 455 |
+
# GH 17527
|
| 456 |
+
df = DataFrame()
|
| 457 |
+
msg = "Invalid value for justify parameter"
|
| 458 |
+
|
| 459 |
+
with pytest.raises(ValueError, match=msg):
|
| 460 |
+
df.to_html(justify=justify)
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
class TestHTMLIndex:
|
| 464 |
+
@pytest.fixture
|
| 465 |
+
def df(self):
|
| 466 |
+
index = ["foo", "bar", "baz"]
|
| 467 |
+
df = DataFrame(
|
| 468 |
+
{"A": [1, 2, 3], "B": [1.2, 3.4, 5.6], "C": ["one", "two", np.nan]},
|
| 469 |
+
columns=["A", "B", "C"],
|
| 470 |
+
index=index,
|
| 471 |
+
)
|
| 472 |
+
return df
|
| 473 |
+
|
| 474 |
+
@pytest.fixture
|
| 475 |
+
def expected_without_index(self, datapath):
|
| 476 |
+
return expected_html(datapath, "index_2")
|
| 477 |
+
|
| 478 |
+
def test_to_html_flat_index_without_name(
|
| 479 |
+
self, datapath, df, expected_without_index
|
| 480 |
+
):
|
| 481 |
+
expected_with_index = expected_html(datapath, "index_1")
|
| 482 |
+
assert df.to_html() == expected_with_index
|
| 483 |
+
|
| 484 |
+
result = df.to_html(index=False)
|
| 485 |
+
for i in df.index:
|
| 486 |
+
assert i not in result
|
| 487 |
+
assert result == expected_without_index
|
| 488 |
+
|
| 489 |
+
def test_to_html_flat_index_with_name(self, datapath, df, expected_without_index):
|
| 490 |
+
df.index = Index(["foo", "bar", "baz"], name="idx")
|
| 491 |
+
expected_with_index = expected_html(datapath, "index_3")
|
| 492 |
+
assert df.to_html() == expected_with_index
|
| 493 |
+
assert df.to_html(index=False) == expected_without_index
|
| 494 |
+
|
| 495 |
+
def test_to_html_multiindex_without_names(
|
| 496 |
+
self, datapath, df, expected_without_index
|
| 497 |
+
):
|
| 498 |
+
tuples = [("foo", "car"), ("foo", "bike"), ("bar", "car")]
|
| 499 |
+
df.index = MultiIndex.from_tuples(tuples)
|
| 500 |
+
|
| 501 |
+
expected_with_index = expected_html(datapath, "index_4")
|
| 502 |
+
assert df.to_html() == expected_with_index
|
| 503 |
+
|
| 504 |
+
result = df.to_html(index=False)
|
| 505 |
+
for i in ["foo", "bar", "car", "bike"]:
|
| 506 |
+
assert i not in result
|
| 507 |
+
# must be the same result as normal index
|
| 508 |
+
assert result == expected_without_index
|
| 509 |
+
|
| 510 |
+
def test_to_html_multiindex_with_names(self, datapath, df, expected_without_index):
|
| 511 |
+
tuples = [("foo", "car"), ("foo", "bike"), ("bar", "car")]
|
| 512 |
+
df.index = MultiIndex.from_tuples(tuples, names=["idx1", "idx2"])
|
| 513 |
+
expected_with_index = expected_html(datapath, "index_5")
|
| 514 |
+
assert df.to_html() == expected_with_index
|
| 515 |
+
assert df.to_html(index=False) == expected_without_index
|
| 516 |
+
|
| 517 |
+
|
| 518 |
+
@pytest.mark.parametrize("classes", ["sortable draggable", ["sortable", "draggable"]])
|
| 519 |
+
def test_to_html_with_classes(classes, datapath):
|
| 520 |
+
df = DataFrame()
|
| 521 |
+
expected = expected_html(datapath, "with_classes")
|
| 522 |
+
result = df.to_html(classes=classes)
|
| 523 |
+
assert result == expected
|
| 524 |
+
|
| 525 |
+
|
| 526 |
+
def test_to_html_no_index_max_rows(datapath):
|
| 527 |
+
# GH 14998
|
| 528 |
+
df = DataFrame({"A": [1, 2, 3, 4]})
|
| 529 |
+
result = df.to_html(index=False, max_rows=1)
|
| 530 |
+
expected = expected_html(datapath, "gh14998_expected_output")
|
| 531 |
+
assert result == expected
|
| 532 |
+
|
| 533 |
+
|
| 534 |
+
def test_to_html_multiindex_max_cols(datapath):
|
| 535 |
+
# GH 6131
|
| 536 |
+
index = MultiIndex(
|
| 537 |
+
levels=[["ba", "bb", "bc"], ["ca", "cb", "cc"]],
|
| 538 |
+
codes=[[0, 1, 2], [0, 1, 2]],
|
| 539 |
+
names=["b", "c"],
|
| 540 |
+
)
|
| 541 |
+
columns = MultiIndex(
|
| 542 |
+
levels=[["d"], ["aa", "ab", "ac"]],
|
| 543 |
+
codes=[[0, 0, 0], [0, 1, 2]],
|
| 544 |
+
names=[None, "a"],
|
| 545 |
+
)
|
| 546 |
+
data = np.array(
|
| 547 |
+
[[1.0, np.nan, np.nan], [np.nan, 2.0, np.nan], [np.nan, np.nan, 3.0]]
|
| 548 |
+
)
|
| 549 |
+
df = DataFrame(data, index, columns)
|
| 550 |
+
result = df.to_html(max_cols=2)
|
| 551 |
+
expected = expected_html(datapath, "gh6131_expected_output")
|
| 552 |
+
assert result == expected
|
| 553 |
+
|
| 554 |
+
|
| 555 |
+
def test_to_html_multi_indexes_index_false(datapath):
|
| 556 |
+
# GH 22579
|
| 557 |
+
df = DataFrame(
|
| 558 |
+
{"a": range(10), "b": range(10, 20), "c": range(10, 20), "d": range(10, 20)}
|
| 559 |
+
)
|
| 560 |
+
df.columns = MultiIndex.from_product([["a", "b"], ["c", "d"]])
|
| 561 |
+
df.index = MultiIndex.from_product([["a", "b"], ["c", "d", "e", "f", "g"]])
|
| 562 |
+
result = df.to_html(index=False)
|
| 563 |
+
expected = expected_html(datapath, "gh22579_expected_output")
|
| 564 |
+
assert result == expected
|
| 565 |
+
|
| 566 |
+
|
| 567 |
+
@pytest.mark.parametrize("index_names", [True, False])
|
| 568 |
+
@pytest.mark.parametrize("header", [True, False])
|
| 569 |
+
@pytest.mark.parametrize("index", [True, False])
|
| 570 |
+
@pytest.mark.parametrize(
|
| 571 |
+
"column_index, column_type",
|
| 572 |
+
[
|
| 573 |
+
(Index([0, 1]), "unnamed_standard"),
|
| 574 |
+
(Index([0, 1], name="columns.name"), "named_standard"),
|
| 575 |
+
(MultiIndex.from_product([["a"], ["b", "c"]]), "unnamed_multi"),
|
| 576 |
+
(
|
| 577 |
+
MultiIndex.from_product(
|
| 578 |
+
[["a"], ["b", "c"]], names=["columns.name.0", "columns.name.1"]
|
| 579 |
+
),
|
| 580 |
+
"named_multi",
|
| 581 |
+
),
|
| 582 |
+
],
|
| 583 |
+
)
|
| 584 |
+
@pytest.mark.parametrize(
|
| 585 |
+
"row_index, row_type",
|
| 586 |
+
[
|
| 587 |
+
(Index([0, 1]), "unnamed_standard"),
|
| 588 |
+
(Index([0, 1], name="index.name"), "named_standard"),
|
| 589 |
+
(MultiIndex.from_product([["a"], ["b", "c"]]), "unnamed_multi"),
|
| 590 |
+
(
|
| 591 |
+
MultiIndex.from_product(
|
| 592 |
+
[["a"], ["b", "c"]], names=["index.name.0", "index.name.1"]
|
| 593 |
+
),
|
| 594 |
+
"named_multi",
|
| 595 |
+
),
|
| 596 |
+
],
|
| 597 |
+
)
|
| 598 |
+
def test_to_html_basic_alignment(
|
| 599 |
+
datapath, row_index, row_type, column_index, column_type, index, header, index_names
|
| 600 |
+
):
|
| 601 |
+
# GH 22747, GH 22579
|
| 602 |
+
df = DataFrame(np.zeros((2, 2), dtype=int), index=row_index, columns=column_index)
|
| 603 |
+
result = df.to_html(index=index, header=header, index_names=index_names)
|
| 604 |
+
|
| 605 |
+
if not index:
|
| 606 |
+
row_type = "none"
|
| 607 |
+
elif not index_names and row_type.startswith("named"):
|
| 608 |
+
row_type = "un" + row_type
|
| 609 |
+
|
| 610 |
+
if not header:
|
| 611 |
+
column_type = "none"
|
| 612 |
+
elif not index_names and column_type.startswith("named"):
|
| 613 |
+
column_type = "un" + column_type
|
| 614 |
+
|
| 615 |
+
filename = "index_" + row_type + "_columns_" + column_type
|
| 616 |
+
expected = expected_html(datapath, filename)
|
| 617 |
+
assert result == expected
|
| 618 |
+
|
| 619 |
+
|
| 620 |
+
@pytest.mark.parametrize("index_names", [True, False])
|
| 621 |
+
@pytest.mark.parametrize("header", [True, False])
|
| 622 |
+
@pytest.mark.parametrize("index", [True, False])
|
| 623 |
+
@pytest.mark.parametrize(
|
| 624 |
+
"column_index, column_type",
|
| 625 |
+
[
|
| 626 |
+
(Index(np.arange(8)), "unnamed_standard"),
|
| 627 |
+
(Index(np.arange(8), name="columns.name"), "named_standard"),
|
| 628 |
+
(
|
| 629 |
+
MultiIndex.from_product([["a", "b"], ["c", "d"], ["e", "f"]]),
|
| 630 |
+
"unnamed_multi",
|
| 631 |
+
),
|
| 632 |
+
(
|
| 633 |
+
MultiIndex.from_product(
|
| 634 |
+
[["a", "b"], ["c", "d"], ["e", "f"]], names=["foo", None, "baz"]
|
| 635 |
+
),
|
| 636 |
+
"named_multi",
|
| 637 |
+
),
|
| 638 |
+
],
|
| 639 |
+
)
|
| 640 |
+
@pytest.mark.parametrize(
|
| 641 |
+
"row_index, row_type",
|
| 642 |
+
[
|
| 643 |
+
(Index(np.arange(8)), "unnamed_standard"),
|
| 644 |
+
(Index(np.arange(8), name="index.name"), "named_standard"),
|
| 645 |
+
(
|
| 646 |
+
MultiIndex.from_product([["a", "b"], ["c", "d"], ["e", "f"]]),
|
| 647 |
+
"unnamed_multi",
|
| 648 |
+
),
|
| 649 |
+
(
|
| 650 |
+
MultiIndex.from_product(
|
| 651 |
+
[["a", "b"], ["c", "d"], ["e", "f"]], names=["foo", None, "baz"]
|
| 652 |
+
),
|
| 653 |
+
"named_multi",
|
| 654 |
+
),
|
| 655 |
+
],
|
| 656 |
+
)
|
| 657 |
+
def test_to_html_alignment_with_truncation(
|
| 658 |
+
datapath, row_index, row_type, column_index, column_type, index, header, index_names
|
| 659 |
+
):
|
| 660 |
+
# GH 22747, GH 22579
|
| 661 |
+
df = DataFrame(np.arange(64).reshape(8, 8), index=row_index, columns=column_index)
|
| 662 |
+
result = df.to_html(
|
| 663 |
+
max_rows=4, max_cols=4, index=index, header=header, index_names=index_names
|
| 664 |
+
)
|
| 665 |
+
|
| 666 |
+
if not index:
|
| 667 |
+
row_type = "none"
|
| 668 |
+
elif not index_names and row_type.startswith("named"):
|
| 669 |
+
row_type = "un" + row_type
|
| 670 |
+
|
| 671 |
+
if not header:
|
| 672 |
+
column_type = "none"
|
| 673 |
+
elif not index_names and column_type.startswith("named"):
|
| 674 |
+
column_type = "un" + column_type
|
| 675 |
+
|
| 676 |
+
filename = "trunc_df_index_" + row_type + "_columns_" + column_type
|
| 677 |
+
expected = expected_html(datapath, filename)
|
| 678 |
+
assert result == expected
|
| 679 |
+
|
| 680 |
+
|
| 681 |
+
@pytest.mark.parametrize("index", [False, 0])
|
| 682 |
+
def test_to_html_truncation_index_false_max_rows(datapath, index):
|
| 683 |
+
# GH 15019
|
| 684 |
+
data = [
|
| 685 |
+
[1.764052, 0.400157],
|
| 686 |
+
[0.978738, 2.240893],
|
| 687 |
+
[1.867558, -0.977278],
|
| 688 |
+
[0.950088, -0.151357],
|
| 689 |
+
[-0.103219, 0.410599],
|
| 690 |
+
]
|
| 691 |
+
df = DataFrame(data)
|
| 692 |
+
result = df.to_html(max_rows=4, index=index)
|
| 693 |
+
expected = expected_html(datapath, "gh15019_expected_output")
|
| 694 |
+
assert result == expected
|
| 695 |
+
|
| 696 |
+
|
| 697 |
+
@pytest.mark.parametrize("index", [False, 0])
|
| 698 |
+
@pytest.mark.parametrize(
|
| 699 |
+
"col_index_named, expected_output",
|
| 700 |
+
[(False, "gh22783_expected_output"), (True, "gh22783_named_columns_index")],
|
| 701 |
+
)
|
| 702 |
+
def test_to_html_truncation_index_false_max_cols(
|
| 703 |
+
datapath, index, col_index_named, expected_output
|
| 704 |
+
):
|
| 705 |
+
# GH 22783
|
| 706 |
+
data = [
|
| 707 |
+
[1.764052, 0.400157, 0.978738, 2.240893, 1.867558],
|
| 708 |
+
[-0.977278, 0.950088, -0.151357, -0.103219, 0.410599],
|
| 709 |
+
]
|
| 710 |
+
df = DataFrame(data)
|
| 711 |
+
if col_index_named:
|
| 712 |
+
df.columns.rename("columns.name", inplace=True)
|
| 713 |
+
result = df.to_html(max_cols=4, index=index)
|
| 714 |
+
expected = expected_html(datapath, expected_output)
|
| 715 |
+
assert result == expected
|
| 716 |
+
|
| 717 |
+
|
| 718 |
+
@pytest.mark.parametrize("notebook", [True, False])
|
| 719 |
+
def test_to_html_notebook_has_style(notebook):
|
| 720 |
+
df = DataFrame({"A": [1, 2, 3]})
|
| 721 |
+
result = df.to_html(notebook=notebook)
|
| 722 |
+
|
| 723 |
+
if notebook:
|
| 724 |
+
assert "tbody tr th:only-of-type" in result
|
| 725 |
+
assert "vertical-align: middle;" in result
|
| 726 |
+
assert "thead th" in result
|
| 727 |
+
else:
|
| 728 |
+
assert "tbody tr th:only-of-type" not in result
|
| 729 |
+
assert "vertical-align: middle;" not in result
|
| 730 |
+
assert "thead th" not in result
|
| 731 |
+
|
| 732 |
+
|
| 733 |
+
def test_to_html_with_index_names_false():
|
| 734 |
+
# GH 16493
|
| 735 |
+
df = DataFrame({"A": [1, 2]}, index=Index(["a", "b"], name="myindexname"))
|
| 736 |
+
result = df.to_html(index_names=False)
|
| 737 |
+
assert "myindexname" not in result
|
| 738 |
+
|
| 739 |
+
|
| 740 |
+
def test_to_html_with_id():
|
| 741 |
+
# GH 8496
|
| 742 |
+
df = DataFrame({"A": [1, 2]}, index=Index(["a", "b"], name="myindexname"))
|
| 743 |
+
result = df.to_html(index_names=False, table_id="TEST_ID")
|
| 744 |
+
assert ' id="TEST_ID"' in result
|
| 745 |
+
|
| 746 |
+
|
| 747 |
+
@pytest.mark.parametrize(
|
| 748 |
+
"value,float_format,expected",
|
| 749 |
+
[
|
| 750 |
+
(0.19999, "%.3f", "gh21625_expected_output"),
|
| 751 |
+
(100.0, "%.0f", "gh22270_expected_output"),
|
| 752 |
+
],
|
| 753 |
+
)
|
| 754 |
+
def test_to_html_float_format_no_fixed_width(value, float_format, expected, datapath):
|
| 755 |
+
# GH 21625, GH 22270
|
| 756 |
+
df = DataFrame({"x": [value]})
|
| 757 |
+
expected = expected_html(datapath, expected)
|
| 758 |
+
result = df.to_html(float_format=float_format)
|
| 759 |
+
assert result == expected
|
| 760 |
+
|
| 761 |
+
|
| 762 |
+
@pytest.mark.parametrize(
|
| 763 |
+
"render_links,expected",
|
| 764 |
+
[(True, "render_links_true"), (False, "render_links_false")],
|
| 765 |
+
)
|
| 766 |
+
def test_to_html_render_links(render_links, expected, datapath):
|
| 767 |
+
# GH 2679
|
| 768 |
+
data = [
|
| 769 |
+
[0, "https://pandas.pydata.org/?q1=a&q2=b", "pydata.org"],
|
| 770 |
+
[0, "www.pydata.org", "pydata.org"],
|
| 771 |
+
]
|
| 772 |
+
df = DataFrame(data, columns=Index(["foo", "bar", None], dtype=object))
|
| 773 |
+
|
| 774 |
+
result = df.to_html(render_links=render_links)
|
| 775 |
+
expected = expected_html(datapath, expected)
|
| 776 |
+
assert result == expected
|
| 777 |
+
|
| 778 |
+
|
| 779 |
+
@pytest.mark.parametrize(
|
| 780 |
+
"method,expected",
|
| 781 |
+
[
|
| 782 |
+
("to_html", lambda x: lorem_ipsum),
|
| 783 |
+
("_repr_html_", lambda x: lorem_ipsum[: x - 4] + "..."), # regression case
|
| 784 |
+
],
|
| 785 |
+
)
|
| 786 |
+
@pytest.mark.parametrize("max_colwidth", [10, 20, 50, 100])
|
| 787 |
+
def test_ignore_display_max_colwidth(method, expected, max_colwidth):
|
| 788 |
+
# see gh-17004
|
| 789 |
+
df = DataFrame([lorem_ipsum])
|
| 790 |
+
with option_context("display.max_colwidth", max_colwidth):
|
| 791 |
+
result = getattr(df, method)()
|
| 792 |
+
expected = expected(max_colwidth)
|
| 793 |
+
assert expected in result
|
| 794 |
+
|
| 795 |
+
|
| 796 |
+
@pytest.mark.parametrize("classes", [True, 0])
|
| 797 |
+
def test_to_html_invalid_classes_type(classes):
|
| 798 |
+
# GH 25608
|
| 799 |
+
df = DataFrame()
|
| 800 |
+
msg = "classes must be a string, list, or tuple"
|
| 801 |
+
|
| 802 |
+
with pytest.raises(TypeError, match=msg):
|
| 803 |
+
df.to_html(classes=classes)
|
| 804 |
+
|
| 805 |
+
|
| 806 |
+
def test_to_html_round_column_headers():
|
| 807 |
+
# GH 17280
|
| 808 |
+
df = DataFrame([1], columns=[0.55555])
|
| 809 |
+
with option_context("display.precision", 3):
|
| 810 |
+
html = df.to_html(notebook=False)
|
| 811 |
+
notebook = df.to_html(notebook=True)
|
| 812 |
+
assert "0.55555" in html
|
| 813 |
+
assert "0.556" in notebook
|
| 814 |
+
|
| 815 |
+
|
| 816 |
+
@pytest.mark.parametrize("unit", ["100px", "10%", "5em", 150])
|
| 817 |
+
def test_to_html_with_col_space_units(unit):
|
| 818 |
+
# GH 25941
|
| 819 |
+
df = DataFrame(np.random.default_rng(2).random(size=(1, 3)))
|
| 820 |
+
result = df.to_html(col_space=unit)
|
| 821 |
+
result = result.split("tbody")[0]
|
| 822 |
+
hdrs = [x for x in result.split("\n") if re.search(r"<th[>\s]", x)]
|
| 823 |
+
if isinstance(unit, int):
|
| 824 |
+
unit = str(unit) + "px"
|
| 825 |
+
for h in hdrs:
|
| 826 |
+
expected = f'<th style="min-width: {unit};">'
|
| 827 |
+
assert expected in h
|
| 828 |
+
|
| 829 |
+
|
| 830 |
+
class TestReprHTML:
|
| 831 |
+
def test_html_repr_min_rows_default(self, datapath):
|
| 832 |
+
# gh-27991
|
| 833 |
+
|
| 834 |
+
# default setting no truncation even if above min_rows
|
| 835 |
+
df = DataFrame({"a": range(20)})
|
| 836 |
+
result = df._repr_html_()
|
| 837 |
+
expected = expected_html(datapath, "html_repr_min_rows_default_no_truncation")
|
| 838 |
+
assert result == expected
|
| 839 |
+
|
| 840 |
+
# default of max_rows 60 triggers truncation if above
|
| 841 |
+
df = DataFrame({"a": range(61)})
|
| 842 |
+
result = df._repr_html_()
|
| 843 |
+
expected = expected_html(datapath, "html_repr_min_rows_default_truncated")
|
| 844 |
+
assert result == expected
|
| 845 |
+
|
| 846 |
+
@pytest.mark.parametrize(
|
| 847 |
+
"max_rows,min_rows,expected",
|
| 848 |
+
[
|
| 849 |
+
# truncated after first two rows
|
| 850 |
+
(10, 4, "html_repr_max_rows_10_min_rows_4"),
|
| 851 |
+
# when set to None, follow value of max_rows
|
| 852 |
+
(12, None, "html_repr_max_rows_12_min_rows_None"),
|
| 853 |
+
# when set value higher as max_rows, use the minimum
|
| 854 |
+
(10, 12, "html_repr_max_rows_10_min_rows_12"),
|
| 855 |
+
# max_rows of None -> never truncate
|
| 856 |
+
(None, 12, "html_repr_max_rows_None_min_rows_12"),
|
| 857 |
+
],
|
| 858 |
+
)
|
| 859 |
+
def test_html_repr_min_rows(self, datapath, max_rows, min_rows, expected):
|
| 860 |
+
# gh-27991
|
| 861 |
+
|
| 862 |
+
df = DataFrame({"a": range(61)})
|
| 863 |
+
expected = expected_html(datapath, expected)
|
| 864 |
+
with option_context("display.max_rows", max_rows, "display.min_rows", min_rows):
|
| 865 |
+
result = df._repr_html_()
|
| 866 |
+
assert result == expected
|
| 867 |
+
|
| 868 |
+
def test_repr_html_ipython_config(self, ip):
|
| 869 |
+
code = textwrap.dedent(
|
| 870 |
+
"""\
|
| 871 |
+
from pandas import DataFrame
|
| 872 |
+
df = DataFrame({"A": [1, 2]})
|
| 873 |
+
df._repr_html_()
|
| 874 |
+
|
| 875 |
+
cfg = get_ipython().config
|
| 876 |
+
cfg['IPKernelApp']['parent_appname']
|
| 877 |
+
df._repr_html_()
|
| 878 |
+
"""
|
| 879 |
+
)
|
| 880 |
+
result = ip.run_cell(code, silent=True)
|
| 881 |
+
assert not result.error_in_exec
|
| 882 |
+
|
| 883 |
+
def test_info_repr_html(self):
|
| 884 |
+
max_rows = 60
|
| 885 |
+
max_cols = 20
|
| 886 |
+
# Long
|
| 887 |
+
h, w = max_rows + 1, max_cols - 1
|
| 888 |
+
df = DataFrame({k: np.arange(1, 1 + h) for k in np.arange(w)})
|
| 889 |
+
assert r"<class" not in df._repr_html_()
|
| 890 |
+
with option_context("display.large_repr", "info"):
|
| 891 |
+
assert r"<class" in df._repr_html_()
|
| 892 |
+
|
| 893 |
+
# Wide
|
| 894 |
+
h, w = max_rows - 1, max_cols + 1
|
| 895 |
+
df = DataFrame({k: np.arange(1, 1 + h) for k in np.arange(w)})
|
| 896 |
+
assert "<class" not in df._repr_html_()
|
| 897 |
+
with option_context(
|
| 898 |
+
"display.large_repr", "info", "display.max_columns", max_cols
|
| 899 |
+
):
|
| 900 |
+
assert "<class" in df._repr_html_()
|
| 901 |
+
|
| 902 |
+
def test_fake_qtconsole_repr_html(self, float_frame):
|
| 903 |
+
df = float_frame
|
| 904 |
+
|
| 905 |
+
def get_ipython():
|
| 906 |
+
return {"config": {"KernelApp": {"parent_appname": "ipython-qtconsole"}}}
|
| 907 |
+
|
| 908 |
+
repstr = df._repr_html_()
|
| 909 |
+
assert repstr is not None
|
| 910 |
+
|
| 911 |
+
with option_context("display.max_rows", 5, "display.max_columns", 2):
|
| 912 |
+
repstr = df._repr_html_()
|
| 913 |
+
|
| 914 |
+
assert "class" in repstr # info fallback
|
| 915 |
+
|
| 916 |
+
def test_repr_html(self, float_frame):
|
| 917 |
+
df = float_frame
|
| 918 |
+
df._repr_html_()
|
| 919 |
+
|
| 920 |
+
with option_context("display.max_rows", 1, "display.max_columns", 1):
|
| 921 |
+
df._repr_html_()
|
| 922 |
+
|
| 923 |
+
with option_context("display.notebook_repr_html", False):
|
| 924 |
+
df._repr_html_()
|
| 925 |
+
|
| 926 |
+
df = DataFrame([[1, 2], [3, 4]])
|
| 927 |
+
with option_context("display.show_dimensions", True):
|
| 928 |
+
assert "2 rows" in df._repr_html_()
|
| 929 |
+
with option_context("display.show_dimensions", False):
|
| 930 |
+
assert "2 rows" not in df._repr_html_()
|
| 931 |
+
|
| 932 |
+
def test_repr_html_mathjax(self):
|
| 933 |
+
df = DataFrame([[1, 2], [3, 4]])
|
| 934 |
+
assert "tex2jax_ignore" not in df._repr_html_()
|
| 935 |
+
assert "mathjax_ignore" not in df._repr_html_()
|
| 936 |
+
|
| 937 |
+
with option_context("display.html.use_mathjax", False):
|
| 938 |
+
assert "tex2jax_ignore" in df._repr_html_()
|
| 939 |
+
assert "mathjax_ignore" in df._repr_html_()
|
| 940 |
+
|
| 941 |
+
def test_repr_html_wide(self):
|
| 942 |
+
max_cols = 20
|
| 943 |
+
df = DataFrame([["a" * 25] * (max_cols - 1)] * 10)
|
| 944 |
+
with option_context("display.max_rows", 60, "display.max_columns", 20):
|
| 945 |
+
assert "..." not in df._repr_html_()
|
| 946 |
+
|
| 947 |
+
wide_df = DataFrame([["a" * 25] * (max_cols + 1)] * 10)
|
| 948 |
+
with option_context("display.max_rows", 60, "display.max_columns", 20):
|
| 949 |
+
assert "..." in wide_df._repr_html_()
|
| 950 |
+
|
| 951 |
+
def test_repr_html_wide_multiindex_cols(self):
|
| 952 |
+
max_cols = 20
|
| 953 |
+
|
| 954 |
+
mcols = MultiIndex.from_product(
|
| 955 |
+
[np.arange(max_cols // 2), ["foo", "bar"]], names=["first", "second"]
|
| 956 |
+
)
|
| 957 |
+
df = DataFrame([["a" * 25] * len(mcols)] * 10, columns=mcols)
|
| 958 |
+
reg_repr = df._repr_html_()
|
| 959 |
+
assert "..." not in reg_repr
|
| 960 |
+
|
| 961 |
+
mcols = MultiIndex.from_product(
|
| 962 |
+
(np.arange(1 + (max_cols // 2)), ["foo", "bar"]), names=["first", "second"]
|
| 963 |
+
)
|
| 964 |
+
df = DataFrame([["a" * 25] * len(mcols)] * 10, columns=mcols)
|
| 965 |
+
with option_context("display.max_rows", 60, "display.max_columns", 20):
|
| 966 |
+
assert "..." in df._repr_html_()
|
| 967 |
+
|
| 968 |
+
def test_repr_html_long(self):
|
| 969 |
+
with option_context("display.max_rows", 60):
|
| 970 |
+
max_rows = get_option("display.max_rows")
|
| 971 |
+
h = max_rows - 1
|
| 972 |
+
df = DataFrame({"A": np.arange(1, 1 + h), "B": np.arange(41, 41 + h)})
|
| 973 |
+
reg_repr = df._repr_html_()
|
| 974 |
+
assert ".." not in reg_repr
|
| 975 |
+
assert str(41 + max_rows // 2) in reg_repr
|
| 976 |
+
|
| 977 |
+
h = max_rows + 1
|
| 978 |
+
df = DataFrame({"A": np.arange(1, 1 + h), "B": np.arange(41, 41 + h)})
|
| 979 |
+
long_repr = df._repr_html_()
|
| 980 |
+
assert ".." in long_repr
|
| 981 |
+
assert str(41 + max_rows // 2) not in long_repr
|
| 982 |
+
assert f"{h} rows " in long_repr
|
| 983 |
+
assert "2 columns" in long_repr
|
| 984 |
+
|
| 985 |
+
def test_repr_html_float(self):
|
| 986 |
+
with option_context("display.max_rows", 60):
|
| 987 |
+
max_rows = get_option("display.max_rows")
|
| 988 |
+
h = max_rows - 1
|
| 989 |
+
df = DataFrame(
|
| 990 |
+
{
|
| 991 |
+
"idx": np.linspace(-10, 10, h),
|
| 992 |
+
"A": np.arange(1, 1 + h),
|
| 993 |
+
"B": np.arange(41, 41 + h),
|
| 994 |
+
}
|
| 995 |
+
).set_index("idx")
|
| 996 |
+
reg_repr = df._repr_html_()
|
| 997 |
+
assert ".." not in reg_repr
|
| 998 |
+
assert f"<td>{40 + h}</td>" in reg_repr
|
| 999 |
+
|
| 1000 |
+
h = max_rows + 1
|
| 1001 |
+
df = DataFrame(
|
| 1002 |
+
{
|
| 1003 |
+
"idx": np.linspace(-10, 10, h),
|
| 1004 |
+
"A": np.arange(1, 1 + h),
|
| 1005 |
+
"B": np.arange(41, 41 + h),
|
| 1006 |
+
}
|
| 1007 |
+
).set_index("idx")
|
| 1008 |
+
long_repr = df._repr_html_()
|
| 1009 |
+
assert ".." in long_repr
|
| 1010 |
+
assert "<td>31</td>" not in long_repr
|
| 1011 |
+
assert f"{h} rows " in long_repr
|
| 1012 |
+
assert "2 columns" in long_repr
|
| 1013 |
+
|
| 1014 |
+
def test_repr_html_long_multiindex(self):
|
| 1015 |
+
max_rows = 60
|
| 1016 |
+
max_L1 = max_rows // 2
|
| 1017 |
+
|
| 1018 |
+
tuples = list(itertools.product(np.arange(max_L1), ["foo", "bar"]))
|
| 1019 |
+
idx = MultiIndex.from_tuples(tuples, names=["first", "second"])
|
| 1020 |
+
df = DataFrame(
|
| 1021 |
+
np.random.default_rng(2).standard_normal((max_L1 * 2, 2)),
|
| 1022 |
+
index=idx,
|
| 1023 |
+
columns=["A", "B"],
|
| 1024 |
+
)
|
| 1025 |
+
with option_context("display.max_rows", 60, "display.max_columns", 20):
|
| 1026 |
+
reg_repr = df._repr_html_()
|
| 1027 |
+
assert "..." not in reg_repr
|
| 1028 |
+
|
| 1029 |
+
tuples = list(itertools.product(np.arange(max_L1 + 1), ["foo", "bar"]))
|
| 1030 |
+
idx = MultiIndex.from_tuples(tuples, names=["first", "second"])
|
| 1031 |
+
df = DataFrame(
|
| 1032 |
+
np.random.default_rng(2).standard_normal(((max_L1 + 1) * 2, 2)),
|
| 1033 |
+
index=idx,
|
| 1034 |
+
columns=["A", "B"],
|
| 1035 |
+
)
|
| 1036 |
+
long_repr = df._repr_html_()
|
| 1037 |
+
assert "..." in long_repr
|
| 1038 |
+
|
| 1039 |
+
def test_repr_html_long_and_wide(self):
|
| 1040 |
+
max_cols = 20
|
| 1041 |
+
max_rows = 60
|
| 1042 |
+
|
| 1043 |
+
h, w = max_rows - 1, max_cols - 1
|
| 1044 |
+
df = DataFrame({k: np.arange(1, 1 + h) for k in np.arange(w)})
|
| 1045 |
+
with option_context("display.max_rows", 60, "display.max_columns", 20):
|
| 1046 |
+
assert "..." not in df._repr_html_()
|
| 1047 |
+
|
| 1048 |
+
h, w = max_rows + 1, max_cols + 1
|
| 1049 |
+
df = DataFrame({k: np.arange(1, 1 + h) for k in np.arange(w)})
|
| 1050 |
+
with option_context("display.max_rows", 60, "display.max_columns", 20):
|
| 1051 |
+
assert "..." in df._repr_html_()
|
| 1052 |
+
|
| 1053 |
+
|
| 1054 |
+
def test_to_html_multilevel(multiindex_year_month_day_dataframe_random_data):
|
| 1055 |
+
ymd = multiindex_year_month_day_dataframe_random_data
|
| 1056 |
+
|
| 1057 |
+
ymd.columns.name = "foo"
|
| 1058 |
+
ymd.to_html()
|
| 1059 |
+
ymd.T.to_html()
|
| 1060 |
+
|
| 1061 |
+
|
| 1062 |
+
@pytest.mark.parametrize("na_rep", ["NaN", "Ted"])
|
| 1063 |
+
def test_to_html_na_rep_and_float_format(na_rep, datapath):
|
| 1064 |
+
# https://github.com/pandas-dev/pandas/issues/13828
|
| 1065 |
+
df = DataFrame(
|
| 1066 |
+
[
|
| 1067 |
+
["A", 1.2225],
|
| 1068 |
+
["A", None],
|
| 1069 |
+
],
|
| 1070 |
+
columns=["Group", "Data"],
|
| 1071 |
+
)
|
| 1072 |
+
result = df.to_html(na_rep=na_rep, float_format="{:.2f}".format)
|
| 1073 |
+
expected = expected_html(datapath, "gh13828_expected_output")
|
| 1074 |
+
expected = expected.format(na_rep=na_rep)
|
| 1075 |
+
assert result == expected
|
| 1076 |
+
|
| 1077 |
+
|
| 1078 |
+
def test_to_html_na_rep_non_scalar_data(datapath):
|
| 1079 |
+
# GH47103
|
| 1080 |
+
df = DataFrame([{"a": 1, "b": [1, 2, 3]}])
|
| 1081 |
+
result = df.to_html(na_rep="-")
|
| 1082 |
+
expected = expected_html(datapath, "gh47103_expected_output")
|
| 1083 |
+
assert result == expected
|
| 1084 |
+
|
| 1085 |
+
|
| 1086 |
+
def test_to_html_float_format_object_col(datapath):
|
| 1087 |
+
# GH#40024
|
| 1088 |
+
df = DataFrame(data={"x": [1000.0, "test"]})
|
| 1089 |
+
result = df.to_html(float_format=lambda x: f"{x:,.0f}")
|
| 1090 |
+
expected = expected_html(datapath, "gh40024_expected_output")
|
| 1091 |
+
assert result == expected
|
| 1092 |
+
|
| 1093 |
+
|
| 1094 |
+
def test_to_html_multiindex_col_with_colspace():
|
| 1095 |
+
# GH#53885
|
| 1096 |
+
df = DataFrame([[1, 2]])
|
| 1097 |
+
df.columns = MultiIndex.from_tuples([(1, 1), (2, 1)])
|
| 1098 |
+
result = df.to_html(col_space=100)
|
| 1099 |
+
expected = (
|
| 1100 |
+
'<table border="1" class="dataframe">\n'
|
| 1101 |
+
" <thead>\n"
|
| 1102 |
+
" <tr>\n"
|
| 1103 |
+
' <th style="min-width: 100px;"></th>\n'
|
| 1104 |
+
' <th style="min-width: 100px;">1</th>\n'
|
| 1105 |
+
' <th style="min-width: 100px;">2</th>\n'
|
| 1106 |
+
" </tr>\n"
|
| 1107 |
+
" <tr>\n"
|
| 1108 |
+
' <th style="min-width: 100px;"></th>\n'
|
| 1109 |
+
' <th style="min-width: 100px;">1</th>\n'
|
| 1110 |
+
' <th style="min-width: 100px;">1</th>\n'
|
| 1111 |
+
" </tr>\n"
|
| 1112 |
+
" </thead>\n"
|
| 1113 |
+
" <tbody>\n"
|
| 1114 |
+
" <tr>\n"
|
| 1115 |
+
" <th>0</th>\n"
|
| 1116 |
+
" <td>1</td>\n"
|
| 1117 |
+
" <td>2</td>\n"
|
| 1118 |
+
" </tr>\n"
|
| 1119 |
+
" </tbody>\n"
|
| 1120 |
+
"</table>"
|
| 1121 |
+
)
|
| 1122 |
+
assert result == expected
|
| 1123 |
+
|
| 1124 |
+
|
| 1125 |
+
def test_to_html_tuple_col_with_colspace():
|
| 1126 |
+
# GH#53885
|
| 1127 |
+
df = DataFrame({("a", "b"): [1], "b": [2]})
|
| 1128 |
+
result = df.to_html(col_space=100)
|
| 1129 |
+
expected = (
|
| 1130 |
+
'<table border="1" class="dataframe">\n'
|
| 1131 |
+
" <thead>\n"
|
| 1132 |
+
' <tr style="text-align: right;">\n'
|
| 1133 |
+
' <th style="min-width: 100px;"></th>\n'
|
| 1134 |
+
' <th style="min-width: 100px;">(a, b)</th>\n'
|
| 1135 |
+
' <th style="min-width: 100px;">b</th>\n'
|
| 1136 |
+
" </tr>\n"
|
| 1137 |
+
" </thead>\n"
|
| 1138 |
+
" <tbody>\n"
|
| 1139 |
+
" <tr>\n"
|
| 1140 |
+
" <th>0</th>\n"
|
| 1141 |
+
" <td>1</td>\n"
|
| 1142 |
+
" <td>2</td>\n"
|
| 1143 |
+
" </tr>\n"
|
| 1144 |
+
" </tbody>\n"
|
| 1145 |
+
"</table>"
|
| 1146 |
+
)
|
| 1147 |
+
assert result == expected
|
| 1148 |
+
|
| 1149 |
+
|
| 1150 |
+
def test_to_html_empty_complex_array():
|
| 1151 |
+
# GH#54167
|
| 1152 |
+
df = DataFrame({"x": np.array([], dtype="complex")})
|
| 1153 |
+
result = df.to_html(col_space=100)
|
| 1154 |
+
expected = (
|
| 1155 |
+
'<table border="1" class="dataframe">\n'
|
| 1156 |
+
" <thead>\n"
|
| 1157 |
+
' <tr style="text-align: right;">\n'
|
| 1158 |
+
' <th style="min-width: 100px;"></th>\n'
|
| 1159 |
+
' <th style="min-width: 100px;">x</th>\n'
|
| 1160 |
+
" </tr>\n"
|
| 1161 |
+
" </thead>\n"
|
| 1162 |
+
" <tbody>\n"
|
| 1163 |
+
" </tbody>\n"
|
| 1164 |
+
"</table>"
|
| 1165 |
+
)
|
| 1166 |
+
assert result == expected
|
.venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_latex.py
ADDED
|
@@ -0,0 +1,1527 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
from textwrap import dedent
|
| 3 |
+
|
| 4 |
+
import pytest
|
| 5 |
+
|
| 6 |
+
import pandas as pd
|
| 7 |
+
from pandas import (
|
| 8 |
+
DataFrame,
|
| 9 |
+
Series,
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
pytest.importorskip("jinja2")
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _dedent(string):
|
| 16 |
+
"""Dedent without new line in the beginning.
|
| 17 |
+
|
| 18 |
+
Built-in textwrap.dedent would keep new line character in the beginning
|
| 19 |
+
of multi-line string starting from the new line.
|
| 20 |
+
This version drops the leading new line character.
|
| 21 |
+
"""
|
| 22 |
+
return dedent(string).lstrip()
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@pytest.fixture
|
| 26 |
+
def df_short():
|
| 27 |
+
"""Short dataframe for testing table/tabular/longtable LaTeX env."""
|
| 28 |
+
return DataFrame({"a": [1, 2], "b": ["b1", "b2"]})
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class TestToLatex:
|
| 32 |
+
def test_to_latex_to_file(self, float_frame, temp_file):
|
| 33 |
+
float_frame.to_latex(temp_file)
|
| 34 |
+
with open(temp_file, encoding="utf-8") as f:
|
| 35 |
+
assert float_frame.to_latex() == f.read()
|
| 36 |
+
|
| 37 |
+
def test_to_latex_to_file_utf8_with_encoding(self, temp_file):
|
| 38 |
+
# test with utf-8 and encoding option (GH 7061)
|
| 39 |
+
df = DataFrame([["au\xdfgangen"]])
|
| 40 |
+
df.to_latex(temp_file, encoding="utf-8")
|
| 41 |
+
with open(temp_file, encoding="utf-8") as f:
|
| 42 |
+
assert df.to_latex() == f.read()
|
| 43 |
+
|
| 44 |
+
def test_to_latex_to_file_utf8_without_encoding(self, temp_file):
|
| 45 |
+
# test with utf-8 without encoding option
|
| 46 |
+
df = DataFrame([["au\xdfgangen"]])
|
| 47 |
+
df.to_latex(temp_file)
|
| 48 |
+
with open(temp_file, encoding="utf-8") as f:
|
| 49 |
+
assert df.to_latex() == f.read()
|
| 50 |
+
|
| 51 |
+
def test_to_latex_tabular_with_index(self):
|
| 52 |
+
df = DataFrame({"a": [1, 2], "b": ["b1", "b2"]})
|
| 53 |
+
result = df.to_latex()
|
| 54 |
+
expected = _dedent(
|
| 55 |
+
r"""
|
| 56 |
+
\begin{tabular}{lrl}
|
| 57 |
+
\toprule
|
| 58 |
+
& a & b \\
|
| 59 |
+
\midrule
|
| 60 |
+
0 & 1 & b1 \\
|
| 61 |
+
1 & 2 & b2 \\
|
| 62 |
+
\bottomrule
|
| 63 |
+
\end{tabular}
|
| 64 |
+
"""
|
| 65 |
+
)
|
| 66 |
+
assert result == expected
|
| 67 |
+
|
| 68 |
+
def test_to_latex_tabular_without_index(self):
|
| 69 |
+
df = DataFrame({"a": [1, 2], "b": ["b1", "b2"]})
|
| 70 |
+
result = df.to_latex(index=False)
|
| 71 |
+
expected = _dedent(
|
| 72 |
+
r"""
|
| 73 |
+
\begin{tabular}{rl}
|
| 74 |
+
\toprule
|
| 75 |
+
a & b \\
|
| 76 |
+
\midrule
|
| 77 |
+
1 & b1 \\
|
| 78 |
+
2 & b2 \\
|
| 79 |
+
\bottomrule
|
| 80 |
+
\end{tabular}
|
| 81 |
+
"""
|
| 82 |
+
)
|
| 83 |
+
assert result == expected
|
| 84 |
+
|
| 85 |
+
@pytest.mark.parametrize(
|
| 86 |
+
"bad_column_format",
|
| 87 |
+
[5, 1.2, ["l", "r"], ("r", "c"), {"r", "c", "l"}, {"a": "r", "b": "l"}],
|
| 88 |
+
)
|
| 89 |
+
def test_to_latex_bad_column_format(self, bad_column_format):
|
| 90 |
+
df = DataFrame({"a": [1, 2], "b": ["b1", "b2"]})
|
| 91 |
+
msg = r"`column_format` must be str or unicode"
|
| 92 |
+
with pytest.raises(ValueError, match=msg):
|
| 93 |
+
df.to_latex(column_format=bad_column_format)
|
| 94 |
+
|
| 95 |
+
def test_to_latex_column_format_just_works(self, float_frame):
|
| 96 |
+
# GH Bug #9402
|
| 97 |
+
float_frame.to_latex(column_format="lcr")
|
| 98 |
+
|
| 99 |
+
def test_to_latex_column_format(self):
|
| 100 |
+
df = DataFrame({"a": [1, 2], "b": ["b1", "b2"]})
|
| 101 |
+
result = df.to_latex(column_format="lcr")
|
| 102 |
+
expected = _dedent(
|
| 103 |
+
r"""
|
| 104 |
+
\begin{tabular}{lcr}
|
| 105 |
+
\toprule
|
| 106 |
+
& a & b \\
|
| 107 |
+
\midrule
|
| 108 |
+
0 & 1 & b1 \\
|
| 109 |
+
1 & 2 & b2 \\
|
| 110 |
+
\bottomrule
|
| 111 |
+
\end{tabular}
|
| 112 |
+
"""
|
| 113 |
+
)
|
| 114 |
+
assert result == expected
|
| 115 |
+
|
| 116 |
+
def test_to_latex_float_format_object_col(self):
|
| 117 |
+
# GH#40024
|
| 118 |
+
ser = Series([1000.0, "test"])
|
| 119 |
+
result = ser.to_latex(float_format="{:,.0f}".format)
|
| 120 |
+
expected = _dedent(
|
| 121 |
+
r"""
|
| 122 |
+
\begin{tabular}{ll}
|
| 123 |
+
\toprule
|
| 124 |
+
& 0 \\
|
| 125 |
+
\midrule
|
| 126 |
+
0 & 1,000 \\
|
| 127 |
+
1 & test \\
|
| 128 |
+
\bottomrule
|
| 129 |
+
\end{tabular}
|
| 130 |
+
"""
|
| 131 |
+
)
|
| 132 |
+
assert result == expected
|
| 133 |
+
|
| 134 |
+
def test_to_latex_empty_tabular(self):
|
| 135 |
+
df = DataFrame()
|
| 136 |
+
result = df.to_latex()
|
| 137 |
+
expected = _dedent(
|
| 138 |
+
r"""
|
| 139 |
+
\begin{tabular}{l}
|
| 140 |
+
\toprule
|
| 141 |
+
\midrule
|
| 142 |
+
\bottomrule
|
| 143 |
+
\end{tabular}
|
| 144 |
+
"""
|
| 145 |
+
)
|
| 146 |
+
assert result == expected
|
| 147 |
+
|
| 148 |
+
def test_to_latex_series(self):
|
| 149 |
+
s = Series(["a", "b", "c"])
|
| 150 |
+
result = s.to_latex()
|
| 151 |
+
expected = _dedent(
|
| 152 |
+
r"""
|
| 153 |
+
\begin{tabular}{ll}
|
| 154 |
+
\toprule
|
| 155 |
+
& 0 \\
|
| 156 |
+
\midrule
|
| 157 |
+
0 & a \\
|
| 158 |
+
1 & b \\
|
| 159 |
+
2 & c \\
|
| 160 |
+
\bottomrule
|
| 161 |
+
\end{tabular}
|
| 162 |
+
"""
|
| 163 |
+
)
|
| 164 |
+
assert result == expected
|
| 165 |
+
|
| 166 |
+
def test_to_latex_midrule_location(self):
|
| 167 |
+
# GH 18326
|
| 168 |
+
df = DataFrame({"a": [1, 2]})
|
| 169 |
+
df.index.name = "foo"
|
| 170 |
+
result = df.to_latex(index_names=False)
|
| 171 |
+
expected = _dedent(
|
| 172 |
+
r"""
|
| 173 |
+
\begin{tabular}{lr}
|
| 174 |
+
\toprule
|
| 175 |
+
& a \\
|
| 176 |
+
\midrule
|
| 177 |
+
0 & 1 \\
|
| 178 |
+
1 & 2 \\
|
| 179 |
+
\bottomrule
|
| 180 |
+
\end{tabular}
|
| 181 |
+
"""
|
| 182 |
+
)
|
| 183 |
+
assert result == expected
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
class TestToLatexLongtable:
|
| 187 |
+
def test_to_latex_empty_longtable(self):
|
| 188 |
+
df = DataFrame()
|
| 189 |
+
result = df.to_latex(longtable=True)
|
| 190 |
+
expected = _dedent(
|
| 191 |
+
r"""
|
| 192 |
+
\begin{longtable}{l}
|
| 193 |
+
\toprule
|
| 194 |
+
\midrule
|
| 195 |
+
\endfirsthead
|
| 196 |
+
\toprule
|
| 197 |
+
\midrule
|
| 198 |
+
\endhead
|
| 199 |
+
\midrule
|
| 200 |
+
\multicolumn{0}{r}{Continued on next page} \\
|
| 201 |
+
\midrule
|
| 202 |
+
\endfoot
|
| 203 |
+
\bottomrule
|
| 204 |
+
\endlastfoot
|
| 205 |
+
\end{longtable}
|
| 206 |
+
"""
|
| 207 |
+
)
|
| 208 |
+
assert result == expected
|
| 209 |
+
|
| 210 |
+
def test_to_latex_longtable_with_index(self):
|
| 211 |
+
df = DataFrame({"a": [1, 2], "b": ["b1", "b2"]})
|
| 212 |
+
result = df.to_latex(longtable=True)
|
| 213 |
+
expected = _dedent(
|
| 214 |
+
r"""
|
| 215 |
+
\begin{longtable}{lrl}
|
| 216 |
+
\toprule
|
| 217 |
+
& a & b \\
|
| 218 |
+
\midrule
|
| 219 |
+
\endfirsthead
|
| 220 |
+
\toprule
|
| 221 |
+
& a & b \\
|
| 222 |
+
\midrule
|
| 223 |
+
\endhead
|
| 224 |
+
\midrule
|
| 225 |
+
\multicolumn{3}{r}{Continued on next page} \\
|
| 226 |
+
\midrule
|
| 227 |
+
\endfoot
|
| 228 |
+
\bottomrule
|
| 229 |
+
\endlastfoot
|
| 230 |
+
0 & 1 & b1 \\
|
| 231 |
+
1 & 2 & b2 \\
|
| 232 |
+
\end{longtable}
|
| 233 |
+
"""
|
| 234 |
+
)
|
| 235 |
+
assert result == expected
|
| 236 |
+
|
| 237 |
+
def test_to_latex_longtable_without_index(self):
|
| 238 |
+
df = DataFrame({"a": [1, 2], "b": ["b1", "b2"]})
|
| 239 |
+
result = df.to_latex(index=False, longtable=True)
|
| 240 |
+
expected = _dedent(
|
| 241 |
+
r"""
|
| 242 |
+
\begin{longtable}{rl}
|
| 243 |
+
\toprule
|
| 244 |
+
a & b \\
|
| 245 |
+
\midrule
|
| 246 |
+
\endfirsthead
|
| 247 |
+
\toprule
|
| 248 |
+
a & b \\
|
| 249 |
+
\midrule
|
| 250 |
+
\endhead
|
| 251 |
+
\midrule
|
| 252 |
+
\multicolumn{2}{r}{Continued on next page} \\
|
| 253 |
+
\midrule
|
| 254 |
+
\endfoot
|
| 255 |
+
\bottomrule
|
| 256 |
+
\endlastfoot
|
| 257 |
+
1 & b1 \\
|
| 258 |
+
2 & b2 \\
|
| 259 |
+
\end{longtable}
|
| 260 |
+
"""
|
| 261 |
+
)
|
| 262 |
+
assert result == expected
|
| 263 |
+
|
| 264 |
+
@pytest.mark.parametrize(
|
| 265 |
+
"df_data, expected_number",
|
| 266 |
+
[
|
| 267 |
+
({"a": [1, 2]}, 1),
|
| 268 |
+
({"a": [1, 2], "b": [3, 4]}, 2),
|
| 269 |
+
({"a": [1, 2], "b": [3, 4], "c": [5, 6]}, 3),
|
| 270 |
+
],
|
| 271 |
+
)
|
| 272 |
+
def test_to_latex_longtable_continued_on_next_page(self, df_data, expected_number):
|
| 273 |
+
df = DataFrame(df_data)
|
| 274 |
+
result = df.to_latex(index=False, longtable=True)
|
| 275 |
+
assert rf"\multicolumn{{{expected_number}}}" in result
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
class TestToLatexHeader:
|
| 279 |
+
def test_to_latex_no_header_with_index(self):
|
| 280 |
+
# GH 7124
|
| 281 |
+
df = DataFrame({"a": [1, 2], "b": ["b1", "b2"]})
|
| 282 |
+
result = df.to_latex(header=False)
|
| 283 |
+
expected = _dedent(
|
| 284 |
+
r"""
|
| 285 |
+
\begin{tabular}{lrl}
|
| 286 |
+
\toprule
|
| 287 |
+
\midrule
|
| 288 |
+
0 & 1 & b1 \\
|
| 289 |
+
1 & 2 & b2 \\
|
| 290 |
+
\bottomrule
|
| 291 |
+
\end{tabular}
|
| 292 |
+
"""
|
| 293 |
+
)
|
| 294 |
+
assert result == expected
|
| 295 |
+
|
| 296 |
+
def test_to_latex_no_header_without_index(self):
|
| 297 |
+
# GH 7124
|
| 298 |
+
df = DataFrame({"a": [1, 2], "b": ["b1", "b2"]})
|
| 299 |
+
result = df.to_latex(index=False, header=False)
|
| 300 |
+
expected = _dedent(
|
| 301 |
+
r"""
|
| 302 |
+
\begin{tabular}{rl}
|
| 303 |
+
\toprule
|
| 304 |
+
\midrule
|
| 305 |
+
1 & b1 \\
|
| 306 |
+
2 & b2 \\
|
| 307 |
+
\bottomrule
|
| 308 |
+
\end{tabular}
|
| 309 |
+
"""
|
| 310 |
+
)
|
| 311 |
+
assert result == expected
|
| 312 |
+
|
| 313 |
+
def test_to_latex_specified_header_with_index(self):
|
| 314 |
+
# GH 7124
|
| 315 |
+
df = DataFrame({"a": [1, 2], "b": ["b1", "b2"]})
|
| 316 |
+
result = df.to_latex(header=["AA", "BB"])
|
| 317 |
+
expected = _dedent(
|
| 318 |
+
r"""
|
| 319 |
+
\begin{tabular}{lrl}
|
| 320 |
+
\toprule
|
| 321 |
+
& AA & BB \\
|
| 322 |
+
\midrule
|
| 323 |
+
0 & 1 & b1 \\
|
| 324 |
+
1 & 2 & b2 \\
|
| 325 |
+
\bottomrule
|
| 326 |
+
\end{tabular}
|
| 327 |
+
"""
|
| 328 |
+
)
|
| 329 |
+
assert result == expected
|
| 330 |
+
|
| 331 |
+
def test_to_latex_specified_header_without_index(self):
|
| 332 |
+
# GH 7124
|
| 333 |
+
df = DataFrame({"a": [1, 2], "b": ["b1", "b2"]})
|
| 334 |
+
result = df.to_latex(header=["AA", "BB"], index=False)
|
| 335 |
+
expected = _dedent(
|
| 336 |
+
r"""
|
| 337 |
+
\begin{tabular}{rl}
|
| 338 |
+
\toprule
|
| 339 |
+
AA & BB \\
|
| 340 |
+
\midrule
|
| 341 |
+
1 & b1 \\
|
| 342 |
+
2 & b2 \\
|
| 343 |
+
\bottomrule
|
| 344 |
+
\end{tabular}
|
| 345 |
+
"""
|
| 346 |
+
)
|
| 347 |
+
assert result == expected
|
| 348 |
+
|
| 349 |
+
@pytest.mark.parametrize(
|
| 350 |
+
"header, num_aliases",
|
| 351 |
+
[
|
| 352 |
+
(["A"], 1),
|
| 353 |
+
(("B",), 1),
|
| 354 |
+
(("Col1", "Col2", "Col3"), 3),
|
| 355 |
+
(("Col1", "Col2", "Col3", "Col4"), 4),
|
| 356 |
+
],
|
| 357 |
+
)
|
| 358 |
+
def test_to_latex_number_of_items_in_header_missmatch_raises(
|
| 359 |
+
self,
|
| 360 |
+
header,
|
| 361 |
+
num_aliases,
|
| 362 |
+
):
|
| 363 |
+
# GH 7124
|
| 364 |
+
df = DataFrame({"a": [1, 2], "b": ["b1", "b2"]})
|
| 365 |
+
msg = f"Writing 2 cols but got {num_aliases} aliases"
|
| 366 |
+
with pytest.raises(ValueError, match=msg):
|
| 367 |
+
df.to_latex(header=header)
|
| 368 |
+
|
| 369 |
+
def test_to_latex_decimal(self):
|
| 370 |
+
# GH 12031
|
| 371 |
+
df = DataFrame({"a": [1.0, 2.1], "b": ["b1", "b2"]})
|
| 372 |
+
result = df.to_latex(decimal=",")
|
| 373 |
+
expected = _dedent(
|
| 374 |
+
r"""
|
| 375 |
+
\begin{tabular}{lrl}
|
| 376 |
+
\toprule
|
| 377 |
+
& a & b \\
|
| 378 |
+
\midrule
|
| 379 |
+
0 & 1,000000 & b1 \\
|
| 380 |
+
1 & 2,100000 & b2 \\
|
| 381 |
+
\bottomrule
|
| 382 |
+
\end{tabular}
|
| 383 |
+
"""
|
| 384 |
+
)
|
| 385 |
+
assert result == expected
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
class TestToLatexBold:
|
| 389 |
+
def test_to_latex_bold_rows(self):
|
| 390 |
+
# GH 16707
|
| 391 |
+
df = DataFrame({"a": [1, 2], "b": ["b1", "b2"]})
|
| 392 |
+
result = df.to_latex(bold_rows=True)
|
| 393 |
+
expected = _dedent(
|
| 394 |
+
r"""
|
| 395 |
+
\begin{tabular}{lrl}
|
| 396 |
+
\toprule
|
| 397 |
+
& a & b \\
|
| 398 |
+
\midrule
|
| 399 |
+
\textbf{0} & 1 & b1 \\
|
| 400 |
+
\textbf{1} & 2 & b2 \\
|
| 401 |
+
\bottomrule
|
| 402 |
+
\end{tabular}
|
| 403 |
+
"""
|
| 404 |
+
)
|
| 405 |
+
assert result == expected
|
| 406 |
+
|
| 407 |
+
def test_to_latex_no_bold_rows(self):
|
| 408 |
+
# GH 16707
|
| 409 |
+
df = DataFrame({"a": [1, 2], "b": ["b1", "b2"]})
|
| 410 |
+
result = df.to_latex(bold_rows=False)
|
| 411 |
+
expected = _dedent(
|
| 412 |
+
r"""
|
| 413 |
+
\begin{tabular}{lrl}
|
| 414 |
+
\toprule
|
| 415 |
+
& a & b \\
|
| 416 |
+
\midrule
|
| 417 |
+
0 & 1 & b1 \\
|
| 418 |
+
1 & 2 & b2 \\
|
| 419 |
+
\bottomrule
|
| 420 |
+
\end{tabular}
|
| 421 |
+
"""
|
| 422 |
+
)
|
| 423 |
+
assert result == expected
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
class TestToLatexCaptionLabel:
|
| 427 |
+
@pytest.fixture
|
| 428 |
+
def caption_table(self):
|
| 429 |
+
"""Caption for table/tabular LaTeX environment."""
|
| 430 |
+
return "a table in a \\texttt{table/tabular} environment"
|
| 431 |
+
|
| 432 |
+
@pytest.fixture
|
| 433 |
+
def short_caption(self):
|
| 434 |
+
"""Short caption for testing \\caption[short_caption]{full_caption}."""
|
| 435 |
+
return "a table"
|
| 436 |
+
|
| 437 |
+
@pytest.fixture
|
| 438 |
+
def label_table(self):
|
| 439 |
+
"""Label for table/tabular LaTeX environment."""
|
| 440 |
+
return "tab:table_tabular"
|
| 441 |
+
|
| 442 |
+
@pytest.fixture
|
| 443 |
+
def caption_longtable(self):
|
| 444 |
+
"""Caption for longtable LaTeX environment."""
|
| 445 |
+
return "a table in a \\texttt{longtable} environment"
|
| 446 |
+
|
| 447 |
+
@pytest.fixture
|
| 448 |
+
def label_longtable(self):
|
| 449 |
+
"""Label for longtable LaTeX environment."""
|
| 450 |
+
return "tab:longtable"
|
| 451 |
+
|
| 452 |
+
def test_to_latex_caption_only(self, df_short, caption_table):
|
| 453 |
+
# GH 25436
|
| 454 |
+
result = df_short.to_latex(caption=caption_table)
|
| 455 |
+
expected = _dedent(
|
| 456 |
+
r"""
|
| 457 |
+
\begin{table}
|
| 458 |
+
\caption{a table in a \texttt{table/tabular} environment}
|
| 459 |
+
\begin{tabular}{lrl}
|
| 460 |
+
\toprule
|
| 461 |
+
& a & b \\
|
| 462 |
+
\midrule
|
| 463 |
+
0 & 1 & b1 \\
|
| 464 |
+
1 & 2 & b2 \\
|
| 465 |
+
\bottomrule
|
| 466 |
+
\end{tabular}
|
| 467 |
+
\end{table}
|
| 468 |
+
"""
|
| 469 |
+
)
|
| 470 |
+
assert result == expected
|
| 471 |
+
|
| 472 |
+
def test_to_latex_label_only(self, df_short, label_table):
|
| 473 |
+
# GH 25436
|
| 474 |
+
result = df_short.to_latex(label=label_table)
|
| 475 |
+
expected = _dedent(
|
| 476 |
+
r"""
|
| 477 |
+
\begin{table}
|
| 478 |
+
\label{tab:table_tabular}
|
| 479 |
+
\begin{tabular}{lrl}
|
| 480 |
+
\toprule
|
| 481 |
+
& a & b \\
|
| 482 |
+
\midrule
|
| 483 |
+
0 & 1 & b1 \\
|
| 484 |
+
1 & 2 & b2 \\
|
| 485 |
+
\bottomrule
|
| 486 |
+
\end{tabular}
|
| 487 |
+
\end{table}
|
| 488 |
+
"""
|
| 489 |
+
)
|
| 490 |
+
assert result == expected
|
| 491 |
+
|
| 492 |
+
def test_to_latex_caption_and_label(self, df_short, caption_table, label_table):
|
| 493 |
+
# GH 25436
|
| 494 |
+
result = df_short.to_latex(caption=caption_table, label=label_table)
|
| 495 |
+
expected = _dedent(
|
| 496 |
+
r"""
|
| 497 |
+
\begin{table}
|
| 498 |
+
\caption{a table in a \texttt{table/tabular} environment}
|
| 499 |
+
\label{tab:table_tabular}
|
| 500 |
+
\begin{tabular}{lrl}
|
| 501 |
+
\toprule
|
| 502 |
+
& a & b \\
|
| 503 |
+
\midrule
|
| 504 |
+
0 & 1 & b1 \\
|
| 505 |
+
1 & 2 & b2 \\
|
| 506 |
+
\bottomrule
|
| 507 |
+
\end{tabular}
|
| 508 |
+
\end{table}
|
| 509 |
+
"""
|
| 510 |
+
)
|
| 511 |
+
assert result == expected
|
| 512 |
+
|
| 513 |
+
def test_to_latex_caption_and_shortcaption(
|
| 514 |
+
self,
|
| 515 |
+
df_short,
|
| 516 |
+
caption_table,
|
| 517 |
+
short_caption,
|
| 518 |
+
):
|
| 519 |
+
result = df_short.to_latex(caption=(caption_table, short_caption))
|
| 520 |
+
expected = _dedent(
|
| 521 |
+
r"""
|
| 522 |
+
\begin{table}
|
| 523 |
+
\caption[a table]{a table in a \texttt{table/tabular} environment}
|
| 524 |
+
\begin{tabular}{lrl}
|
| 525 |
+
\toprule
|
| 526 |
+
& a & b \\
|
| 527 |
+
\midrule
|
| 528 |
+
0 & 1 & b1 \\
|
| 529 |
+
1 & 2 & b2 \\
|
| 530 |
+
\bottomrule
|
| 531 |
+
\end{tabular}
|
| 532 |
+
\end{table}
|
| 533 |
+
"""
|
| 534 |
+
)
|
| 535 |
+
assert result == expected
|
| 536 |
+
|
| 537 |
+
def test_to_latex_caption_and_shortcaption_list_is_ok(self, df_short):
|
| 538 |
+
caption = ("Long-long-caption", "Short")
|
| 539 |
+
result_tuple = df_short.to_latex(caption=caption)
|
| 540 |
+
result_list = df_short.to_latex(caption=list(caption))
|
| 541 |
+
assert result_tuple == result_list
|
| 542 |
+
|
| 543 |
+
def test_to_latex_caption_shortcaption_and_label(
|
| 544 |
+
self,
|
| 545 |
+
df_short,
|
| 546 |
+
caption_table,
|
| 547 |
+
short_caption,
|
| 548 |
+
label_table,
|
| 549 |
+
):
|
| 550 |
+
# test when the short_caption is provided alongside caption and label
|
| 551 |
+
result = df_short.to_latex(
|
| 552 |
+
caption=(caption_table, short_caption),
|
| 553 |
+
label=label_table,
|
| 554 |
+
)
|
| 555 |
+
expected = _dedent(
|
| 556 |
+
r"""
|
| 557 |
+
\begin{table}
|
| 558 |
+
\caption[a table]{a table in a \texttt{table/tabular} environment}
|
| 559 |
+
\label{tab:table_tabular}
|
| 560 |
+
\begin{tabular}{lrl}
|
| 561 |
+
\toprule
|
| 562 |
+
& a & b \\
|
| 563 |
+
\midrule
|
| 564 |
+
0 & 1 & b1 \\
|
| 565 |
+
1 & 2 & b2 \\
|
| 566 |
+
\bottomrule
|
| 567 |
+
\end{tabular}
|
| 568 |
+
\end{table}
|
| 569 |
+
"""
|
| 570 |
+
)
|
| 571 |
+
assert result == expected
|
| 572 |
+
|
| 573 |
+
@pytest.mark.parametrize(
|
| 574 |
+
"bad_caption",
|
| 575 |
+
[
|
| 576 |
+
("full_caption", "short_caption", "extra_string"),
|
| 577 |
+
("full_caption", "short_caption", 1),
|
| 578 |
+
("full_caption", "short_caption", None),
|
| 579 |
+
("full_caption",),
|
| 580 |
+
(None,),
|
| 581 |
+
],
|
| 582 |
+
)
|
| 583 |
+
def test_to_latex_bad_caption_raises(self, bad_caption):
|
| 584 |
+
# test that wrong number of params is raised
|
| 585 |
+
df = DataFrame({"a": [1]})
|
| 586 |
+
msg = "`caption` must be either a string or 2-tuple of strings"
|
| 587 |
+
with pytest.raises(ValueError, match=msg):
|
| 588 |
+
df.to_latex(caption=bad_caption)
|
| 589 |
+
|
| 590 |
+
def test_to_latex_two_chars_caption(self, df_short):
|
| 591 |
+
# test that two chars caption is handled correctly
|
| 592 |
+
# it must not be unpacked into long_caption, short_caption.
|
| 593 |
+
result = df_short.to_latex(caption="xy")
|
| 594 |
+
expected = _dedent(
|
| 595 |
+
r"""
|
| 596 |
+
\begin{table}
|
| 597 |
+
\caption{xy}
|
| 598 |
+
\begin{tabular}{lrl}
|
| 599 |
+
\toprule
|
| 600 |
+
& a & b \\
|
| 601 |
+
\midrule
|
| 602 |
+
0 & 1 & b1 \\
|
| 603 |
+
1 & 2 & b2 \\
|
| 604 |
+
\bottomrule
|
| 605 |
+
\end{tabular}
|
| 606 |
+
\end{table}
|
| 607 |
+
"""
|
| 608 |
+
)
|
| 609 |
+
assert result == expected
|
| 610 |
+
|
| 611 |
+
def test_to_latex_longtable_caption_only(self, df_short, caption_longtable):
|
| 612 |
+
# GH 25436
|
| 613 |
+
# test when no caption and no label is provided
|
| 614 |
+
# is performed by test_to_latex_longtable()
|
| 615 |
+
result = df_short.to_latex(longtable=True, caption=caption_longtable)
|
| 616 |
+
expected = _dedent(
|
| 617 |
+
r"""
|
| 618 |
+
\begin{longtable}{lrl}
|
| 619 |
+
\caption{a table in a \texttt{longtable} environment} \\
|
| 620 |
+
\toprule
|
| 621 |
+
& a & b \\
|
| 622 |
+
\midrule
|
| 623 |
+
\endfirsthead
|
| 624 |
+
\caption[]{a table in a \texttt{longtable} environment} \\
|
| 625 |
+
\toprule
|
| 626 |
+
& a & b \\
|
| 627 |
+
\midrule
|
| 628 |
+
\endhead
|
| 629 |
+
\midrule
|
| 630 |
+
\multicolumn{3}{r}{Continued on next page} \\
|
| 631 |
+
\midrule
|
| 632 |
+
\endfoot
|
| 633 |
+
\bottomrule
|
| 634 |
+
\endlastfoot
|
| 635 |
+
0 & 1 & b1 \\
|
| 636 |
+
1 & 2 & b2 \\
|
| 637 |
+
\end{longtable}
|
| 638 |
+
"""
|
| 639 |
+
)
|
| 640 |
+
assert result == expected
|
| 641 |
+
|
| 642 |
+
def test_to_latex_longtable_label_only(self, df_short, label_longtable):
|
| 643 |
+
# GH 25436
|
| 644 |
+
result = df_short.to_latex(longtable=True, label=label_longtable)
|
| 645 |
+
expected = _dedent(
|
| 646 |
+
r"""
|
| 647 |
+
\begin{longtable}{lrl}
|
| 648 |
+
\label{tab:longtable} \\
|
| 649 |
+
\toprule
|
| 650 |
+
& a & b \\
|
| 651 |
+
\midrule
|
| 652 |
+
\endfirsthead
|
| 653 |
+
\toprule
|
| 654 |
+
& a & b \\
|
| 655 |
+
\midrule
|
| 656 |
+
\endhead
|
| 657 |
+
\midrule
|
| 658 |
+
\multicolumn{3}{r}{Continued on next page} \\
|
| 659 |
+
\midrule
|
| 660 |
+
\endfoot
|
| 661 |
+
\bottomrule
|
| 662 |
+
\endlastfoot
|
| 663 |
+
0 & 1 & b1 \\
|
| 664 |
+
1 & 2 & b2 \\
|
| 665 |
+
\end{longtable}
|
| 666 |
+
"""
|
| 667 |
+
)
|
| 668 |
+
assert result == expected
|
| 669 |
+
|
| 670 |
+
def test_to_latex_longtable_caption_and_label(
|
| 671 |
+
self,
|
| 672 |
+
df_short,
|
| 673 |
+
caption_longtable,
|
| 674 |
+
label_longtable,
|
| 675 |
+
):
|
| 676 |
+
# GH 25436
|
| 677 |
+
result = df_short.to_latex(
|
| 678 |
+
longtable=True,
|
| 679 |
+
caption=caption_longtable,
|
| 680 |
+
label=label_longtable,
|
| 681 |
+
)
|
| 682 |
+
expected = _dedent(
|
| 683 |
+
r"""
|
| 684 |
+
\begin{longtable}{lrl}
|
| 685 |
+
\caption{a table in a \texttt{longtable} environment} \label{tab:longtable} \\
|
| 686 |
+
\toprule
|
| 687 |
+
& a & b \\
|
| 688 |
+
\midrule
|
| 689 |
+
\endfirsthead
|
| 690 |
+
\caption[]{a table in a \texttt{longtable} environment} \\
|
| 691 |
+
\toprule
|
| 692 |
+
& a & b \\
|
| 693 |
+
\midrule
|
| 694 |
+
\endhead
|
| 695 |
+
\midrule
|
| 696 |
+
\multicolumn{3}{r}{Continued on next page} \\
|
| 697 |
+
\midrule
|
| 698 |
+
\endfoot
|
| 699 |
+
\bottomrule
|
| 700 |
+
\endlastfoot
|
| 701 |
+
0 & 1 & b1 \\
|
| 702 |
+
1 & 2 & b2 \\
|
| 703 |
+
\end{longtable}
|
| 704 |
+
"""
|
| 705 |
+
)
|
| 706 |
+
assert result == expected
|
| 707 |
+
|
| 708 |
+
def test_to_latex_longtable_caption_shortcaption_and_label(
|
| 709 |
+
self,
|
| 710 |
+
df_short,
|
| 711 |
+
caption_longtable,
|
| 712 |
+
short_caption,
|
| 713 |
+
label_longtable,
|
| 714 |
+
):
|
| 715 |
+
# test when the caption, the short_caption and the label are provided
|
| 716 |
+
result = df_short.to_latex(
|
| 717 |
+
longtable=True,
|
| 718 |
+
caption=(caption_longtable, short_caption),
|
| 719 |
+
label=label_longtable,
|
| 720 |
+
)
|
| 721 |
+
expected = _dedent(
|
| 722 |
+
r"""
|
| 723 |
+
\begin{longtable}{lrl}
|
| 724 |
+
\caption[a table]{a table in a \texttt{longtable} environment} \label{tab:longtable} \\
|
| 725 |
+
\toprule
|
| 726 |
+
& a & b \\
|
| 727 |
+
\midrule
|
| 728 |
+
\endfirsthead
|
| 729 |
+
\caption[]{a table in a \texttt{longtable} environment} \\
|
| 730 |
+
\toprule
|
| 731 |
+
& a & b \\
|
| 732 |
+
\midrule
|
| 733 |
+
\endhead
|
| 734 |
+
\midrule
|
| 735 |
+
\multicolumn{3}{r}{Continued on next page} \\
|
| 736 |
+
\midrule
|
| 737 |
+
\endfoot
|
| 738 |
+
\bottomrule
|
| 739 |
+
\endlastfoot
|
| 740 |
+
0 & 1 & b1 \\
|
| 741 |
+
1 & 2 & b2 \\
|
| 742 |
+
\end{longtable}
|
| 743 |
+
"""
|
| 744 |
+
)
|
| 745 |
+
assert result == expected
|
| 746 |
+
|
| 747 |
+
|
| 748 |
+
class TestToLatexEscape:
|
| 749 |
+
@pytest.fixture
|
| 750 |
+
def df_with_symbols(self):
|
| 751 |
+
"""Dataframe with special characters for testing chars escaping."""
|
| 752 |
+
a = "a"
|
| 753 |
+
b = "b"
|
| 754 |
+
return DataFrame({"co$e^x$": {a: "a", b: "b"}, "co^l1": {a: "a", b: "b"}})
|
| 755 |
+
|
| 756 |
+
def test_to_latex_escape_false(self, df_with_symbols):
|
| 757 |
+
result = df_with_symbols.to_latex(escape=False)
|
| 758 |
+
expected = _dedent(
|
| 759 |
+
r"""
|
| 760 |
+
\begin{tabular}{lll}
|
| 761 |
+
\toprule
|
| 762 |
+
& co$e^x$ & co^l1 \\
|
| 763 |
+
\midrule
|
| 764 |
+
a & a & a \\
|
| 765 |
+
b & b & b \\
|
| 766 |
+
\bottomrule
|
| 767 |
+
\end{tabular}
|
| 768 |
+
"""
|
| 769 |
+
)
|
| 770 |
+
assert result == expected
|
| 771 |
+
|
| 772 |
+
def test_to_latex_escape_default(self, df_with_symbols):
|
| 773 |
+
# gh50871: in v2.0 escape is False by default (styler.format.escape=None)
|
| 774 |
+
default = df_with_symbols.to_latex()
|
| 775 |
+
specified_true = df_with_symbols.to_latex(escape=True)
|
| 776 |
+
assert default != specified_true
|
| 777 |
+
|
| 778 |
+
def test_to_latex_special_escape(self):
|
| 779 |
+
df = DataFrame([r"a\b\c", r"^a^b^c", r"~a~b~c"])
|
| 780 |
+
result = df.to_latex(escape=True)
|
| 781 |
+
expected = _dedent(
|
| 782 |
+
r"""
|
| 783 |
+
\begin{tabular}{ll}
|
| 784 |
+
\toprule
|
| 785 |
+
& 0 \\
|
| 786 |
+
\midrule
|
| 787 |
+
0 & a\textbackslash b\textbackslash c \\
|
| 788 |
+
1 & \textasciicircum a\textasciicircum b\textasciicircum c \\
|
| 789 |
+
2 & \textasciitilde a\textasciitilde b\textasciitilde c \\
|
| 790 |
+
\bottomrule
|
| 791 |
+
\end{tabular}
|
| 792 |
+
"""
|
| 793 |
+
)
|
| 794 |
+
assert result == expected
|
| 795 |
+
|
| 796 |
+
def test_to_latex_escape_special_chars(self):
|
| 797 |
+
special_characters = ["&", "%", "$", "#", "_", "{", "}", "~", "^", "\\"]
|
| 798 |
+
df = DataFrame(data=special_characters)
|
| 799 |
+
result = df.to_latex(escape=True)
|
| 800 |
+
expected = _dedent(
|
| 801 |
+
r"""
|
| 802 |
+
\begin{tabular}{ll}
|
| 803 |
+
\toprule
|
| 804 |
+
& 0 \\
|
| 805 |
+
\midrule
|
| 806 |
+
0 & \& \\
|
| 807 |
+
1 & \% \\
|
| 808 |
+
2 & \$ \\
|
| 809 |
+
3 & \# \\
|
| 810 |
+
4 & \_ \\
|
| 811 |
+
5 & \{ \\
|
| 812 |
+
6 & \} \\
|
| 813 |
+
7 & \textasciitilde \\
|
| 814 |
+
8 & \textasciicircum \\
|
| 815 |
+
9 & \textbackslash \\
|
| 816 |
+
\bottomrule
|
| 817 |
+
\end{tabular}
|
| 818 |
+
"""
|
| 819 |
+
)
|
| 820 |
+
assert result == expected
|
| 821 |
+
|
| 822 |
+
def test_to_latex_escape_special_chars_in_index_names(self):
|
| 823 |
+
# https://github.com/pandas-dev/pandas/issues/61309
|
| 824 |
+
# https://github.com/pandas-dev/pandas/issues/57362
|
| 825 |
+
index = "&%$#_{}}~^\\"
|
| 826 |
+
df = DataFrame({index: [1, 2, 3]}).set_index(index)
|
| 827 |
+
result = df.to_latex(escape=True)
|
| 828 |
+
expected = _dedent(
|
| 829 |
+
r"""
|
| 830 |
+
\begin{tabular}{l}
|
| 831 |
+
\toprule
|
| 832 |
+
\&\%\$\#\_\{\}\}\textasciitilde \textasciicircum \textbackslash \\
|
| 833 |
+
\midrule
|
| 834 |
+
1 \\
|
| 835 |
+
2 \\
|
| 836 |
+
3 \\
|
| 837 |
+
\bottomrule
|
| 838 |
+
\end{tabular}
|
| 839 |
+
"""
|
| 840 |
+
)
|
| 841 |
+
assert result == expected
|
| 842 |
+
|
| 843 |
+
def test_to_latex_escape_special_chars_in_column_name(self):
|
| 844 |
+
df = DataFrame({"A": [1, 2, 3], "B": ["a", "b", "c"]})
|
| 845 |
+
df.columns.name = "_^~"
|
| 846 |
+
result = df.to_latex(escape=True)
|
| 847 |
+
expected = _dedent(
|
| 848 |
+
r"""
|
| 849 |
+
\begin{tabular}{lrl}
|
| 850 |
+
\toprule
|
| 851 |
+
\_\textasciicircum \textasciitilde & A & B \\
|
| 852 |
+
\midrule
|
| 853 |
+
0 & 1 & a \\
|
| 854 |
+
1 & 2 & b \\
|
| 855 |
+
2 & 3 & c \\
|
| 856 |
+
\bottomrule
|
| 857 |
+
\end{tabular}
|
| 858 |
+
"""
|
| 859 |
+
)
|
| 860 |
+
assert result == expected
|
| 861 |
+
|
| 862 |
+
def test_to_latex_specified_header_special_chars_without_escape(self):
|
| 863 |
+
# GH 7124
|
| 864 |
+
df = DataFrame({"a": [1, 2], "b": ["b1", "b2"]})
|
| 865 |
+
result = df.to_latex(header=["$A$", "$B$"], escape=False)
|
| 866 |
+
expected = _dedent(
|
| 867 |
+
r"""
|
| 868 |
+
\begin{tabular}{lrl}
|
| 869 |
+
\toprule
|
| 870 |
+
& $A$ & $B$ \\
|
| 871 |
+
\midrule
|
| 872 |
+
0 & 1 & b1 \\
|
| 873 |
+
1 & 2 & b2 \\
|
| 874 |
+
\bottomrule
|
| 875 |
+
\end{tabular}
|
| 876 |
+
"""
|
| 877 |
+
)
|
| 878 |
+
assert result == expected
|
| 879 |
+
|
| 880 |
+
|
| 881 |
+
class TestToLatexPosition:
|
| 882 |
+
def test_to_latex_position(self):
|
| 883 |
+
the_position = "h"
|
| 884 |
+
df = DataFrame({"a": [1, 2], "b": ["b1", "b2"]})
|
| 885 |
+
result = df.to_latex(position=the_position)
|
| 886 |
+
expected = _dedent(
|
| 887 |
+
r"""
|
| 888 |
+
\begin{table}[h]
|
| 889 |
+
\begin{tabular}{lrl}
|
| 890 |
+
\toprule
|
| 891 |
+
& a & b \\
|
| 892 |
+
\midrule
|
| 893 |
+
0 & 1 & b1 \\
|
| 894 |
+
1 & 2 & b2 \\
|
| 895 |
+
\bottomrule
|
| 896 |
+
\end{tabular}
|
| 897 |
+
\end{table}
|
| 898 |
+
"""
|
| 899 |
+
)
|
| 900 |
+
assert result == expected
|
| 901 |
+
|
| 902 |
+
def test_to_latex_longtable_position(self):
|
| 903 |
+
the_position = "t"
|
| 904 |
+
df = DataFrame({"a": [1, 2], "b": ["b1", "b2"]})
|
| 905 |
+
result = df.to_latex(longtable=True, position=the_position)
|
| 906 |
+
expected = _dedent(
|
| 907 |
+
r"""
|
| 908 |
+
\begin{longtable}[t]{lrl}
|
| 909 |
+
\toprule
|
| 910 |
+
& a & b \\
|
| 911 |
+
\midrule
|
| 912 |
+
\endfirsthead
|
| 913 |
+
\toprule
|
| 914 |
+
& a & b \\
|
| 915 |
+
\midrule
|
| 916 |
+
\endhead
|
| 917 |
+
\midrule
|
| 918 |
+
\multicolumn{3}{r}{Continued on next page} \\
|
| 919 |
+
\midrule
|
| 920 |
+
\endfoot
|
| 921 |
+
\bottomrule
|
| 922 |
+
\endlastfoot
|
| 923 |
+
0 & 1 & b1 \\
|
| 924 |
+
1 & 2 & b2 \\
|
| 925 |
+
\end{longtable}
|
| 926 |
+
"""
|
| 927 |
+
)
|
| 928 |
+
assert result == expected
|
| 929 |
+
|
| 930 |
+
|
| 931 |
+
class TestToLatexFormatters:
|
| 932 |
+
def test_to_latex_with_formatters(self):
|
| 933 |
+
df = DataFrame(
|
| 934 |
+
{
|
| 935 |
+
"datetime64": [
|
| 936 |
+
datetime(2016, 1, 1),
|
| 937 |
+
datetime(2016, 2, 5),
|
| 938 |
+
datetime(2016, 3, 3),
|
| 939 |
+
],
|
| 940 |
+
"float": [1.0, 2.0, 3.0],
|
| 941 |
+
"int": [1, 2, 3],
|
| 942 |
+
"object": [(1, 2), True, False],
|
| 943 |
+
}
|
| 944 |
+
)
|
| 945 |
+
|
| 946 |
+
formatters = {
|
| 947 |
+
"datetime64": lambda x: x.strftime("%Y-%m"),
|
| 948 |
+
"float": lambda x: f"[{x: 4.1f}]",
|
| 949 |
+
"int": lambda x: f"0x{x:x}",
|
| 950 |
+
"object": lambda x: f"-{x!s}-",
|
| 951 |
+
"__index__": lambda x: f"index: {x}",
|
| 952 |
+
}
|
| 953 |
+
result = df.to_latex(formatters=dict(formatters))
|
| 954 |
+
|
| 955 |
+
expected = _dedent(
|
| 956 |
+
r"""
|
| 957 |
+
\begin{tabular}{llrrl}
|
| 958 |
+
\toprule
|
| 959 |
+
& datetime64 & float & int & object \\
|
| 960 |
+
\midrule
|
| 961 |
+
index: 0 & 2016-01 & [ 1.0] & 0x1 & -(1, 2)- \\
|
| 962 |
+
index: 1 & 2016-02 & [ 2.0] & 0x2 & -True- \\
|
| 963 |
+
index: 2 & 2016-03 & [ 3.0] & 0x3 & -False- \\
|
| 964 |
+
\bottomrule
|
| 965 |
+
\end{tabular}
|
| 966 |
+
"""
|
| 967 |
+
)
|
| 968 |
+
assert result == expected
|
| 969 |
+
|
| 970 |
+
def test_to_latex_float_format_no_fixed_width_3decimals(self):
|
| 971 |
+
# GH 21625
|
| 972 |
+
df = DataFrame({"x": [0.19999]})
|
| 973 |
+
result = df.to_latex(float_format="%.3f")
|
| 974 |
+
expected = _dedent(
|
| 975 |
+
r"""
|
| 976 |
+
\begin{tabular}{lr}
|
| 977 |
+
\toprule
|
| 978 |
+
& x \\
|
| 979 |
+
\midrule
|
| 980 |
+
0 & 0.200 \\
|
| 981 |
+
\bottomrule
|
| 982 |
+
\end{tabular}
|
| 983 |
+
"""
|
| 984 |
+
)
|
| 985 |
+
assert result == expected
|
| 986 |
+
|
| 987 |
+
def test_to_latex_float_format_no_fixed_width_integer(self):
|
| 988 |
+
# GH 22270
|
| 989 |
+
df = DataFrame({"x": [100.0]})
|
| 990 |
+
result = df.to_latex(float_format="%.0f")
|
| 991 |
+
expected = _dedent(
|
| 992 |
+
r"""
|
| 993 |
+
\begin{tabular}{lr}
|
| 994 |
+
\toprule
|
| 995 |
+
& x \\
|
| 996 |
+
\midrule
|
| 997 |
+
0 & 100 \\
|
| 998 |
+
\bottomrule
|
| 999 |
+
\end{tabular}
|
| 1000 |
+
"""
|
| 1001 |
+
)
|
| 1002 |
+
assert result == expected
|
| 1003 |
+
|
| 1004 |
+
@pytest.mark.parametrize("na_rep", ["NaN", "Ted"])
|
| 1005 |
+
def test_to_latex_na_rep_and_float_format(self, na_rep):
|
| 1006 |
+
df = DataFrame(
|
| 1007 |
+
[
|
| 1008 |
+
["A", 1.2225],
|
| 1009 |
+
["A", None],
|
| 1010 |
+
],
|
| 1011 |
+
columns=["Group", "Data"],
|
| 1012 |
+
)
|
| 1013 |
+
result = df.to_latex(na_rep=na_rep, float_format="{:.2f}".format)
|
| 1014 |
+
expected = _dedent(
|
| 1015 |
+
rf"""
|
| 1016 |
+
\begin{{tabular}}{{llr}}
|
| 1017 |
+
\toprule
|
| 1018 |
+
& Group & Data \\
|
| 1019 |
+
\midrule
|
| 1020 |
+
0 & A & 1.22 \\
|
| 1021 |
+
1 & A & {na_rep} \\
|
| 1022 |
+
\bottomrule
|
| 1023 |
+
\end{{tabular}}
|
| 1024 |
+
"""
|
| 1025 |
+
)
|
| 1026 |
+
assert result == expected
|
| 1027 |
+
|
| 1028 |
+
|
| 1029 |
+
class TestToLatexMultiindex:
|
| 1030 |
+
@pytest.fixture
|
| 1031 |
+
def multiindex_frame(self):
|
| 1032 |
+
"""Multiindex dataframe for testing multirow LaTeX macros."""
|
| 1033 |
+
return DataFrame.from_dict(
|
| 1034 |
+
{
|
| 1035 |
+
("c1", 0): Series({x: x for x in range(4)}),
|
| 1036 |
+
("c1", 1): Series({x: x + 4 for x in range(4)}),
|
| 1037 |
+
("c2", 0): Series({x: x for x in range(4)}),
|
| 1038 |
+
("c2", 1): Series({x: x + 4 for x in range(4)}),
|
| 1039 |
+
("c3", 0): Series({x: x for x in range(4)}),
|
| 1040 |
+
}
|
| 1041 |
+
).T
|
| 1042 |
+
|
| 1043 |
+
@pytest.fixture
|
| 1044 |
+
def multicolumn_frame(self):
|
| 1045 |
+
"""Multicolumn dataframe for testing multicolumn LaTeX macros."""
|
| 1046 |
+
return DataFrame(
|
| 1047 |
+
{
|
| 1048 |
+
("c1", 0): {x: x for x in range(5)},
|
| 1049 |
+
("c1", 1): {x: x + 5 for x in range(5)},
|
| 1050 |
+
("c2", 0): {x: x for x in range(5)},
|
| 1051 |
+
("c2", 1): {x: x + 5 for x in range(5)},
|
| 1052 |
+
("c3", 0): {x: x for x in range(5)},
|
| 1053 |
+
}
|
| 1054 |
+
)
|
| 1055 |
+
|
| 1056 |
+
def test_to_latex_multindex_header(self):
|
| 1057 |
+
# GH 16718
|
| 1058 |
+
df = DataFrame({"a": [0], "b": [1], "c": [2], "d": [3]})
|
| 1059 |
+
df = df.set_index(["a", "b"])
|
| 1060 |
+
observed = df.to_latex(header=["r1", "r2"], multirow=False)
|
| 1061 |
+
expected = _dedent(
|
| 1062 |
+
r"""
|
| 1063 |
+
\begin{tabular}{llrr}
|
| 1064 |
+
\toprule
|
| 1065 |
+
& & r1 & r2 \\
|
| 1066 |
+
a & b & & \\
|
| 1067 |
+
\midrule
|
| 1068 |
+
0 & 1 & 2 & 3 \\
|
| 1069 |
+
\bottomrule
|
| 1070 |
+
\end{tabular}
|
| 1071 |
+
"""
|
| 1072 |
+
)
|
| 1073 |
+
assert observed == expected
|
| 1074 |
+
|
| 1075 |
+
def test_to_latex_multiindex_empty_name(self):
|
| 1076 |
+
# GH 18669
|
| 1077 |
+
mi = pd.MultiIndex.from_product([[1, 2]], names=[""])
|
| 1078 |
+
df = DataFrame(-1, index=mi, columns=range(4))
|
| 1079 |
+
observed = df.to_latex()
|
| 1080 |
+
expected = _dedent(
|
| 1081 |
+
r"""
|
| 1082 |
+
\begin{tabular}{lrrrr}
|
| 1083 |
+
\toprule
|
| 1084 |
+
& 0 & 1 & 2 & 3 \\
|
| 1085 |
+
& & & & \\
|
| 1086 |
+
\midrule
|
| 1087 |
+
1 & -1 & -1 & -1 & -1 \\
|
| 1088 |
+
2 & -1 & -1 & -1 & -1 \\
|
| 1089 |
+
\bottomrule
|
| 1090 |
+
\end{tabular}
|
| 1091 |
+
"""
|
| 1092 |
+
)
|
| 1093 |
+
assert observed == expected
|
| 1094 |
+
|
| 1095 |
+
def test_to_latex_multiindex_column_tabular(self):
|
| 1096 |
+
df = DataFrame({("x", "y"): ["a"]})
|
| 1097 |
+
result = df.to_latex()
|
| 1098 |
+
expected = _dedent(
|
| 1099 |
+
r"""
|
| 1100 |
+
\begin{tabular}{ll}
|
| 1101 |
+
\toprule
|
| 1102 |
+
& x \\
|
| 1103 |
+
& y \\
|
| 1104 |
+
\midrule
|
| 1105 |
+
0 & a \\
|
| 1106 |
+
\bottomrule
|
| 1107 |
+
\end{tabular}
|
| 1108 |
+
"""
|
| 1109 |
+
)
|
| 1110 |
+
assert result == expected
|
| 1111 |
+
|
| 1112 |
+
def test_to_latex_multiindex_small_tabular(self):
|
| 1113 |
+
df = DataFrame({("x", "y"): ["a"]}).T
|
| 1114 |
+
result = df.to_latex(multirow=False)
|
| 1115 |
+
expected = _dedent(
|
| 1116 |
+
r"""
|
| 1117 |
+
\begin{tabular}{lll}
|
| 1118 |
+
\toprule
|
| 1119 |
+
& & 0 \\
|
| 1120 |
+
\midrule
|
| 1121 |
+
x & y & a \\
|
| 1122 |
+
\bottomrule
|
| 1123 |
+
\end{tabular}
|
| 1124 |
+
"""
|
| 1125 |
+
)
|
| 1126 |
+
assert result == expected
|
| 1127 |
+
|
| 1128 |
+
def test_to_latex_multiindex_tabular(self, multiindex_frame):
|
| 1129 |
+
result = multiindex_frame.to_latex(multirow=False)
|
| 1130 |
+
expected = _dedent(
|
| 1131 |
+
r"""
|
| 1132 |
+
\begin{tabular}{llrrrr}
|
| 1133 |
+
\toprule
|
| 1134 |
+
& & 0 & 1 & 2 & 3 \\
|
| 1135 |
+
\midrule
|
| 1136 |
+
c1 & 0 & 0 & 1 & 2 & 3 \\
|
| 1137 |
+
& 1 & 4 & 5 & 6 & 7 \\
|
| 1138 |
+
c2 & 0 & 0 & 1 & 2 & 3 \\
|
| 1139 |
+
& 1 & 4 & 5 & 6 & 7 \\
|
| 1140 |
+
c3 & 0 & 0 & 1 & 2 & 3 \\
|
| 1141 |
+
\bottomrule
|
| 1142 |
+
\end{tabular}
|
| 1143 |
+
"""
|
| 1144 |
+
)
|
| 1145 |
+
assert result == expected
|
| 1146 |
+
|
| 1147 |
+
def test_to_latex_multicolumn_tabular(self, multiindex_frame):
|
| 1148 |
+
# GH 14184
|
| 1149 |
+
df = multiindex_frame.T
|
| 1150 |
+
df.columns.names = ["a", "b"]
|
| 1151 |
+
result = df.to_latex(multirow=False)
|
| 1152 |
+
expected = _dedent(
|
| 1153 |
+
r"""
|
| 1154 |
+
\begin{tabular}{lrrrrr}
|
| 1155 |
+
\toprule
|
| 1156 |
+
a & \multicolumn{2}{r}{c1} & \multicolumn{2}{r}{c2} & c3 \\
|
| 1157 |
+
b & 0 & 1 & 0 & 1 & 0 \\
|
| 1158 |
+
\midrule
|
| 1159 |
+
0 & 0 & 4 & 0 & 4 & 0 \\
|
| 1160 |
+
1 & 1 & 5 & 1 & 5 & 1 \\
|
| 1161 |
+
2 & 2 & 6 & 2 & 6 & 2 \\
|
| 1162 |
+
3 & 3 & 7 & 3 & 7 & 3 \\
|
| 1163 |
+
\bottomrule
|
| 1164 |
+
\end{tabular}
|
| 1165 |
+
"""
|
| 1166 |
+
)
|
| 1167 |
+
assert result == expected
|
| 1168 |
+
|
| 1169 |
+
def test_to_latex_index_has_name_tabular(self):
|
| 1170 |
+
# GH 10660
|
| 1171 |
+
df = DataFrame({"a": [0, 0, 1, 1], "b": list("abab"), "c": [1, 2, 3, 4]})
|
| 1172 |
+
result = df.set_index(["a", "b"]).to_latex(multirow=False)
|
| 1173 |
+
expected = _dedent(
|
| 1174 |
+
r"""
|
| 1175 |
+
\begin{tabular}{llr}
|
| 1176 |
+
\toprule
|
| 1177 |
+
& & c \\
|
| 1178 |
+
a & b & \\
|
| 1179 |
+
\midrule
|
| 1180 |
+
0 & a & 1 \\
|
| 1181 |
+
& b & 2 \\
|
| 1182 |
+
1 & a & 3 \\
|
| 1183 |
+
& b & 4 \\
|
| 1184 |
+
\bottomrule
|
| 1185 |
+
\end{tabular}
|
| 1186 |
+
"""
|
| 1187 |
+
)
|
| 1188 |
+
assert result == expected
|
| 1189 |
+
|
| 1190 |
+
def test_to_latex_groupby_tabular(self):
|
| 1191 |
+
# GH 10660
|
| 1192 |
+
df = DataFrame({"a": [0, 0, 1, 1], "b": list("abab"), "c": [1, 2, 3, 4]})
|
| 1193 |
+
result = (
|
| 1194 |
+
df.groupby("a")
|
| 1195 |
+
.describe()
|
| 1196 |
+
.to_latex(float_format="{:.1f}".format, escape=True)
|
| 1197 |
+
)
|
| 1198 |
+
expected = _dedent(
|
| 1199 |
+
r"""
|
| 1200 |
+
\begin{tabular}{lrrrrrrrr}
|
| 1201 |
+
\toprule
|
| 1202 |
+
& \multicolumn{8}{r}{c} \\
|
| 1203 |
+
& count & mean & std & min & 25\% & 50\% & 75\% & max \\
|
| 1204 |
+
a & & & & & & & & \\
|
| 1205 |
+
\midrule
|
| 1206 |
+
0 & 2.0 & 1.5 & 0.7 & 1.0 & 1.2 & 1.5 & 1.8 & 2.0 \\
|
| 1207 |
+
1 & 2.0 & 3.5 & 0.7 & 3.0 & 3.2 & 3.5 & 3.8 & 4.0 \\
|
| 1208 |
+
\bottomrule
|
| 1209 |
+
\end{tabular}
|
| 1210 |
+
"""
|
| 1211 |
+
)
|
| 1212 |
+
assert result == expected
|
| 1213 |
+
|
| 1214 |
+
def test_to_latex_multiindex_dupe_level(self):
|
| 1215 |
+
# see gh-14484
|
| 1216 |
+
#
|
| 1217 |
+
# If an index is repeated in subsequent rows, it should be
|
| 1218 |
+
# replaced with a blank in the created table. This should
|
| 1219 |
+
# ONLY happen if all higher order indices (to the left) are
|
| 1220 |
+
# equal too. In this test, 'c' has to be printed both times
|
| 1221 |
+
# because the higher order index 'A' != 'B'.
|
| 1222 |
+
df = DataFrame(
|
| 1223 |
+
index=pd.MultiIndex.from_tuples([("A", "c"), ("B", "c")]), columns=["col"]
|
| 1224 |
+
)
|
| 1225 |
+
result = df.to_latex(multirow=False)
|
| 1226 |
+
expected = _dedent(
|
| 1227 |
+
r"""
|
| 1228 |
+
\begin{tabular}{lll}
|
| 1229 |
+
\toprule
|
| 1230 |
+
& & col \\
|
| 1231 |
+
\midrule
|
| 1232 |
+
A & c & NaN \\
|
| 1233 |
+
B & c & NaN \\
|
| 1234 |
+
\bottomrule
|
| 1235 |
+
\end{tabular}
|
| 1236 |
+
"""
|
| 1237 |
+
)
|
| 1238 |
+
assert result == expected
|
| 1239 |
+
|
| 1240 |
+
def test_to_latex_multicolumn_default(self, multicolumn_frame):
|
| 1241 |
+
result = multicolumn_frame.to_latex()
|
| 1242 |
+
expected = _dedent(
|
| 1243 |
+
r"""
|
| 1244 |
+
\begin{tabular}{lrrrrr}
|
| 1245 |
+
\toprule
|
| 1246 |
+
& \multicolumn{2}{r}{c1} & \multicolumn{2}{r}{c2} & c3 \\
|
| 1247 |
+
& 0 & 1 & 0 & 1 & 0 \\
|
| 1248 |
+
\midrule
|
| 1249 |
+
0 & 0 & 5 & 0 & 5 & 0 \\
|
| 1250 |
+
1 & 1 & 6 & 1 & 6 & 1 \\
|
| 1251 |
+
2 & 2 & 7 & 2 & 7 & 2 \\
|
| 1252 |
+
3 & 3 & 8 & 3 & 8 & 3 \\
|
| 1253 |
+
4 & 4 & 9 & 4 & 9 & 4 \\
|
| 1254 |
+
\bottomrule
|
| 1255 |
+
\end{tabular}
|
| 1256 |
+
"""
|
| 1257 |
+
)
|
| 1258 |
+
assert result == expected
|
| 1259 |
+
|
| 1260 |
+
def test_to_latex_multicolumn_false(self, multicolumn_frame):
|
| 1261 |
+
result = multicolumn_frame.to_latex(multicolumn=False, multicolumn_format="l")
|
| 1262 |
+
expected = _dedent(
|
| 1263 |
+
r"""
|
| 1264 |
+
\begin{tabular}{lrrrrr}
|
| 1265 |
+
\toprule
|
| 1266 |
+
& c1 & & c2 & & c3 \\
|
| 1267 |
+
& 0 & 1 & 0 & 1 & 0 \\
|
| 1268 |
+
\midrule
|
| 1269 |
+
0 & 0 & 5 & 0 & 5 & 0 \\
|
| 1270 |
+
1 & 1 & 6 & 1 & 6 & 1 \\
|
| 1271 |
+
2 & 2 & 7 & 2 & 7 & 2 \\
|
| 1272 |
+
3 & 3 & 8 & 3 & 8 & 3 \\
|
| 1273 |
+
4 & 4 & 9 & 4 & 9 & 4 \\
|
| 1274 |
+
\bottomrule
|
| 1275 |
+
\end{tabular}
|
| 1276 |
+
"""
|
| 1277 |
+
)
|
| 1278 |
+
assert result == expected
|
| 1279 |
+
|
| 1280 |
+
def test_to_latex_multirow_true(self, multicolumn_frame):
|
| 1281 |
+
result = multicolumn_frame.T.to_latex(multirow=True)
|
| 1282 |
+
expected = _dedent(
|
| 1283 |
+
r"""
|
| 1284 |
+
\begin{tabular}{llrrrrr}
|
| 1285 |
+
\toprule
|
| 1286 |
+
& & 0 & 1 & 2 & 3 & 4 \\
|
| 1287 |
+
\midrule
|
| 1288 |
+
\multirow[t]{2}{*}{c1} & 0 & 0 & 1 & 2 & 3 & 4 \\
|
| 1289 |
+
& 1 & 5 & 6 & 7 & 8 & 9 \\
|
| 1290 |
+
\cline{1-7}
|
| 1291 |
+
\multirow[t]{2}{*}{c2} & 0 & 0 & 1 & 2 & 3 & 4 \\
|
| 1292 |
+
& 1 & 5 & 6 & 7 & 8 & 9 \\
|
| 1293 |
+
\cline{1-7}
|
| 1294 |
+
c3 & 0 & 0 & 1 & 2 & 3 & 4 \\
|
| 1295 |
+
\cline{1-7}
|
| 1296 |
+
\bottomrule
|
| 1297 |
+
\end{tabular}
|
| 1298 |
+
"""
|
| 1299 |
+
)
|
| 1300 |
+
assert result == expected
|
| 1301 |
+
|
| 1302 |
+
def test_to_latex_multicolumnrow_with_multicol_format(self, multicolumn_frame):
|
| 1303 |
+
multicolumn_frame.index = multicolumn_frame.T.index
|
| 1304 |
+
result = multicolumn_frame.T.to_latex(
|
| 1305 |
+
multirow=True,
|
| 1306 |
+
multicolumn=True,
|
| 1307 |
+
multicolumn_format="c",
|
| 1308 |
+
)
|
| 1309 |
+
expected = _dedent(
|
| 1310 |
+
r"""
|
| 1311 |
+
\begin{tabular}{llrrrrr}
|
| 1312 |
+
\toprule
|
| 1313 |
+
& & \multicolumn{2}{c}{c1} & \multicolumn{2}{c}{c2} & c3 \\
|
| 1314 |
+
& & 0 & 1 & 0 & 1 & 0 \\
|
| 1315 |
+
\midrule
|
| 1316 |
+
\multirow[t]{2}{*}{c1} & 0 & 0 & 1 & 2 & 3 & 4 \\
|
| 1317 |
+
& 1 & 5 & 6 & 7 & 8 & 9 \\
|
| 1318 |
+
\cline{1-7}
|
| 1319 |
+
\multirow[t]{2}{*}{c2} & 0 & 0 & 1 & 2 & 3 & 4 \\
|
| 1320 |
+
& 1 & 5 & 6 & 7 & 8 & 9 \\
|
| 1321 |
+
\cline{1-7}
|
| 1322 |
+
c3 & 0 & 0 & 1 & 2 & 3 & 4 \\
|
| 1323 |
+
\cline{1-7}
|
| 1324 |
+
\bottomrule
|
| 1325 |
+
\end{tabular}
|
| 1326 |
+
"""
|
| 1327 |
+
)
|
| 1328 |
+
assert result == expected
|
| 1329 |
+
|
| 1330 |
+
@pytest.mark.parametrize("name0", [None, "named0"])
|
| 1331 |
+
@pytest.mark.parametrize("name1", [None, "named1"])
|
| 1332 |
+
@pytest.mark.parametrize("axes", [[0], [1], [0, 1]])
|
| 1333 |
+
def test_to_latex_multiindex_names(self, name0, name1, axes):
|
| 1334 |
+
# GH 18667
|
| 1335 |
+
names = [name0, name1]
|
| 1336 |
+
mi = pd.MultiIndex.from_product([[1, 2], [3, 4]])
|
| 1337 |
+
df = DataFrame(-1, index=mi.copy(), columns=mi.copy())
|
| 1338 |
+
for idx in axes:
|
| 1339 |
+
df.axes[idx].names = names
|
| 1340 |
+
|
| 1341 |
+
idx_names = tuple(n or "" for n in names)
|
| 1342 |
+
idx_names_row = (
|
| 1343 |
+
f"{idx_names[0]} & {idx_names[1]} & & & & \\\\\n"
|
| 1344 |
+
if (0 in axes and any(names))
|
| 1345 |
+
else ""
|
| 1346 |
+
)
|
| 1347 |
+
col_names = [n if (bool(n) and 1 in axes) else "" for n in names]
|
| 1348 |
+
observed = df.to_latex(multirow=False)
|
| 1349 |
+
expected = r"""\begin{{tabular}}{{llrrrr}}
|
| 1350 |
+
\toprule
|
| 1351 |
+
& {} & \multicolumn{{2}}{{r}}{{1}} & \multicolumn{{2}}{{r}}{{2}} \\
|
| 1352 |
+
& {} & 3 & 4 & 3 & 4 \\
|
| 1353 |
+
{}\midrule
|
| 1354 |
+
1 & 3 & -1 & -1 & -1 & -1 \\
|
| 1355 |
+
& 4 & -1 & -1 & -1 & -1 \\
|
| 1356 |
+
2 & 3 & -1 & -1 & -1 & -1 \\
|
| 1357 |
+
& 4 & -1 & -1 & -1 & -1 \\
|
| 1358 |
+
\bottomrule
|
| 1359 |
+
\end{{tabular}}
|
| 1360 |
+
""".format(*col_names, idx_names_row)
|
| 1361 |
+
assert observed == expected
|
| 1362 |
+
|
| 1363 |
+
@pytest.mark.parametrize("one_row", [True, False])
|
| 1364 |
+
def test_to_latex_multiindex_nans(self, one_row):
|
| 1365 |
+
# GH 14249
|
| 1366 |
+
df = DataFrame({"a": [None, 1], "b": [2, 3], "c": [4, 5]})
|
| 1367 |
+
if one_row:
|
| 1368 |
+
df = df.iloc[[0]]
|
| 1369 |
+
observed = df.set_index(["a", "b"]).to_latex(multirow=False)
|
| 1370 |
+
expected = _dedent(
|
| 1371 |
+
r"""
|
| 1372 |
+
\begin{tabular}{llr}
|
| 1373 |
+
\toprule
|
| 1374 |
+
& & c \\
|
| 1375 |
+
a & b & \\
|
| 1376 |
+
\midrule
|
| 1377 |
+
NaN & 2 & 4 \\
|
| 1378 |
+
"""
|
| 1379 |
+
)
|
| 1380 |
+
if not one_row:
|
| 1381 |
+
expected += r"""1.000000 & 3 & 5 \\
|
| 1382 |
+
"""
|
| 1383 |
+
expected += r"""\bottomrule
|
| 1384 |
+
\end{tabular}
|
| 1385 |
+
"""
|
| 1386 |
+
assert observed == expected
|
| 1387 |
+
|
| 1388 |
+
def test_to_latex_non_string_index(self):
|
| 1389 |
+
# GH 19981
|
| 1390 |
+
df = DataFrame([[1, 2, 3]] * 2).set_index([0, 1])
|
| 1391 |
+
result = df.to_latex(multirow=False)
|
| 1392 |
+
expected = _dedent(
|
| 1393 |
+
r"""
|
| 1394 |
+
\begin{tabular}{llr}
|
| 1395 |
+
\toprule
|
| 1396 |
+
& & 2 \\
|
| 1397 |
+
0 & 1 & \\
|
| 1398 |
+
\midrule
|
| 1399 |
+
1 & 2 & 3 \\
|
| 1400 |
+
& 2 & 3 \\
|
| 1401 |
+
\bottomrule
|
| 1402 |
+
\end{tabular}
|
| 1403 |
+
"""
|
| 1404 |
+
)
|
| 1405 |
+
assert result == expected
|
| 1406 |
+
|
| 1407 |
+
def test_to_latex_multiindex_multirow(self):
|
| 1408 |
+
# GH 16719
|
| 1409 |
+
mi = pd.MultiIndex.from_product(
|
| 1410 |
+
[[0.0, 1.0], [3.0, 2.0, 1.0], ["0", "1"]], names=["i", "val0", "val1"]
|
| 1411 |
+
)
|
| 1412 |
+
df = DataFrame(index=mi)
|
| 1413 |
+
result = df.to_latex(multirow=True, escape=False)
|
| 1414 |
+
expected = _dedent(
|
| 1415 |
+
r"""
|
| 1416 |
+
\begin{tabular}{lll}
|
| 1417 |
+
\toprule
|
| 1418 |
+
i & val0 & val1 \\
|
| 1419 |
+
\midrule
|
| 1420 |
+
\multirow[t]{6}{*}{0.000000} & \multirow[t]{2}{*}{3.000000} & 0 \\
|
| 1421 |
+
& & 1 \\
|
| 1422 |
+
\cline{2-3}
|
| 1423 |
+
& \multirow[t]{2}{*}{2.000000} & 0 \\
|
| 1424 |
+
& & 1 \\
|
| 1425 |
+
\cline{2-3}
|
| 1426 |
+
& \multirow[t]{2}{*}{1.000000} & 0 \\
|
| 1427 |
+
& & 1 \\
|
| 1428 |
+
\cline{1-3} \cline{2-3}
|
| 1429 |
+
\multirow[t]{6}{*}{1.000000} & \multirow[t]{2}{*}{3.000000} & 0 \\
|
| 1430 |
+
& & 1 \\
|
| 1431 |
+
\cline{2-3}
|
| 1432 |
+
& \multirow[t]{2}{*}{2.000000} & 0 \\
|
| 1433 |
+
& & 1 \\
|
| 1434 |
+
\cline{2-3}
|
| 1435 |
+
& \multirow[t]{2}{*}{1.000000} & 0 \\
|
| 1436 |
+
& & 1 \\
|
| 1437 |
+
\cline{1-3} \cline{2-3}
|
| 1438 |
+
\bottomrule
|
| 1439 |
+
\end{tabular}
|
| 1440 |
+
"""
|
| 1441 |
+
)
|
| 1442 |
+
assert result == expected
|
| 1443 |
+
|
| 1444 |
+
def test_to_latex_multiindex_format_single_index_hidden(self):
|
| 1445 |
+
# GH 52218
|
| 1446 |
+
df = DataFrame(
|
| 1447 |
+
{
|
| 1448 |
+
"A": [1, 2],
|
| 1449 |
+
"B": [4, 5],
|
| 1450 |
+
}
|
| 1451 |
+
)
|
| 1452 |
+
result = (
|
| 1453 |
+
df.style.hide(axis="index")
|
| 1454 |
+
.map_index(lambda v: "textbf:--rwrap;", axis="columns")
|
| 1455 |
+
.to_latex()
|
| 1456 |
+
)
|
| 1457 |
+
expected = _dedent(r"""
|
| 1458 |
+
\begin{tabular}{rr}
|
| 1459 |
+
\textbf{A} & \textbf{B} \\
|
| 1460 |
+
1 & 4 \\
|
| 1461 |
+
2 & 5 \\
|
| 1462 |
+
\end{tabular}
|
| 1463 |
+
""")
|
| 1464 |
+
assert result == expected
|
| 1465 |
+
|
| 1466 |
+
def test_to_latex_multiindex_format_triple_index_two_hidden(self):
|
| 1467 |
+
# GH 52218
|
| 1468 |
+
arrays = [
|
| 1469 |
+
["A", "A", "B", "B"],
|
| 1470 |
+
["one", "two", "one", "two"],
|
| 1471 |
+
["x", "x", "y", "y"],
|
| 1472 |
+
]
|
| 1473 |
+
index = pd.MultiIndex.from_arrays(
|
| 1474 |
+
arrays, names=["Level 0", "Level 1", "Level 2"]
|
| 1475 |
+
)
|
| 1476 |
+
df = DataFrame(
|
| 1477 |
+
[[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]],
|
| 1478 |
+
index=index,
|
| 1479 |
+
columns=["C1", "C2", "C3"],
|
| 1480 |
+
)
|
| 1481 |
+
result = (
|
| 1482 |
+
df.style.hide(axis="index", level=[0, 1])
|
| 1483 |
+
.map_index(lambda v: "textbf:--rwrap;", axis="columns")
|
| 1484 |
+
.to_latex()
|
| 1485 |
+
)
|
| 1486 |
+
expected = _dedent(r"""
|
| 1487 |
+
\begin{tabular}{lrrr}
|
| 1488 |
+
& \textbf{C1} & \textbf{C2} & \textbf{C3} \\
|
| 1489 |
+
Level 2 & & & \\
|
| 1490 |
+
x & 0 & 0 & 0 \\
|
| 1491 |
+
x & 0 & 0 & 0 \\
|
| 1492 |
+
y & 0 & 0 & 0 \\
|
| 1493 |
+
y & 0 & 0 & 0 \\
|
| 1494 |
+
\end{tabular}
|
| 1495 |
+
""")
|
| 1496 |
+
assert result == expected
|
| 1497 |
+
|
| 1498 |
+
def test_to_latex_multiindex_format_triple_index_all_hidden(self):
|
| 1499 |
+
# GH 52218
|
| 1500 |
+
arrays = [
|
| 1501 |
+
["A", "A", "B", "B"],
|
| 1502 |
+
["one", "two", "one", "two"],
|
| 1503 |
+
["x", "x", "y", "y"],
|
| 1504 |
+
]
|
| 1505 |
+
index = pd.MultiIndex.from_arrays(
|
| 1506 |
+
arrays, names=["Level 0", "Level 1", "Level 2"]
|
| 1507 |
+
)
|
| 1508 |
+
df = DataFrame(
|
| 1509 |
+
[[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]],
|
| 1510 |
+
index=index,
|
| 1511 |
+
columns=["C1", "C2", "C3"],
|
| 1512 |
+
)
|
| 1513 |
+
result = (
|
| 1514 |
+
df.style.hide(axis="index", level=[0, 1, 2])
|
| 1515 |
+
.map_index(lambda v: "textbf:--rwrap;", axis="columns")
|
| 1516 |
+
.to_latex()
|
| 1517 |
+
)
|
| 1518 |
+
expected = _dedent(r"""
|
| 1519 |
+
\begin{tabular}{rrr}
|
| 1520 |
+
\textbf{C1} & \textbf{C2} & \textbf{C3} \\
|
| 1521 |
+
0 & 0 & 0 \\
|
| 1522 |
+
0 & 0 & 0 \\
|
| 1523 |
+
0 & 0 & 0 \\
|
| 1524 |
+
0 & 0 & 0 \\
|
| 1525 |
+
\end{tabular}
|
| 1526 |
+
""")
|
| 1527 |
+
assert result == expected
|
.venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_markdown.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from io import StringIO
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
|
| 5 |
+
from pandas.errors import Pandas4Warning
|
| 6 |
+
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import pandas._testing as tm
|
| 9 |
+
|
| 10 |
+
pytest.importorskip("tabulate")
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def test_keyword_deprecation():
|
| 14 |
+
# GH 57280
|
| 15 |
+
msg = (
|
| 16 |
+
"Starting with pandas version 4.0 all arguments of to_markdown "
|
| 17 |
+
"except for the argument 'buf' will be keyword-only."
|
| 18 |
+
)
|
| 19 |
+
s = pd.Series()
|
| 20 |
+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
|
| 21 |
+
s.to_markdown(None, "wt")
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def test_simple():
|
| 25 |
+
buf = StringIO()
|
| 26 |
+
df = pd.DataFrame([1, 2, 3])
|
| 27 |
+
df.to_markdown(buf=buf)
|
| 28 |
+
result = buf.getvalue()
|
| 29 |
+
assert (
|
| 30 |
+
result == "| | 0 |\n|---:|----:|\n| 0 | 1 |\n| 1 | 2 |\n| 2 | 3 |"
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def test_empty_frame():
|
| 35 |
+
buf = StringIO()
|
| 36 |
+
df = pd.DataFrame({"id": [], "first_name": [], "last_name": []}).set_index("id")
|
| 37 |
+
df.to_markdown(buf=buf)
|
| 38 |
+
result = buf.getvalue()
|
| 39 |
+
assert result == (
|
| 40 |
+
"| id | first_name | last_name |\n|------|--------------|-------------|"
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def test_other_tablefmt():
|
| 45 |
+
buf = StringIO()
|
| 46 |
+
df = pd.DataFrame([1, 2, 3])
|
| 47 |
+
df.to_markdown(buf=buf, tablefmt="jira")
|
| 48 |
+
result = buf.getvalue()
|
| 49 |
+
assert result == "|| || 0 ||\n| 0 | 1 |\n| 1 | 2 |\n| 2 | 3 |"
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def test_other_headers():
|
| 53 |
+
buf = StringIO()
|
| 54 |
+
df = pd.DataFrame([1, 2, 3])
|
| 55 |
+
df.to_markdown(buf=buf, headers=["foo", "bar"])
|
| 56 |
+
result = buf.getvalue()
|
| 57 |
+
assert result == (
|
| 58 |
+
"| foo | bar |\n|------:|------:|\n| 0 "
|
| 59 |
+
"| 1 |\n| 1 | 2 |\n| 2 | 3 |"
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def test_series():
|
| 64 |
+
buf = StringIO()
|
| 65 |
+
s = pd.Series([1, 2, 3], name="foo")
|
| 66 |
+
s.to_markdown(buf=buf)
|
| 67 |
+
result = buf.getvalue()
|
| 68 |
+
assert result == (
|
| 69 |
+
"| | foo |\n|---:|------:|\n| 0 | 1 |\n| 1 | 2 |\n| 2 | 3 |"
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def test_no_buf():
|
| 74 |
+
df = pd.DataFrame([1, 2, 3])
|
| 75 |
+
result = df.to_markdown()
|
| 76 |
+
assert (
|
| 77 |
+
result == "| | 0 |\n|---:|----:|\n| 0 | 1 |\n| 1 | 2 |\n| 2 | 3 |"
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
@pytest.mark.parametrize("index", [True, False])
|
| 82 |
+
def test_index(index):
|
| 83 |
+
# GH 32667
|
| 84 |
+
|
| 85 |
+
df = pd.DataFrame([1, 2, 3])
|
| 86 |
+
|
| 87 |
+
result = df.to_markdown(index=index)
|
| 88 |
+
|
| 89 |
+
if index:
|
| 90 |
+
expected = (
|
| 91 |
+
"| | 0 |\n|---:|----:|\n| 0 | 1 |\n| 1 | 2 |\n| 2 | 3 |"
|
| 92 |
+
)
|
| 93 |
+
else:
|
| 94 |
+
expected = "| 0 |\n|----:|\n| 1 |\n| 2 |\n| 3 |"
|
| 95 |
+
assert result == expected
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def test_showindex_disallowed_in_kwargs():
|
| 99 |
+
# GH 32667; disallowing showindex in kwargs enforced in 2.0
|
| 100 |
+
df = pd.DataFrame([1, 2, 3])
|
| 101 |
+
with pytest.raises(ValueError, match="Pass 'index' instead of 'showindex"):
|
| 102 |
+
df.to_markdown(index=True, showindex=True)
|
.venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_string.py
ADDED
|
@@ -0,0 +1,1226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import (
|
| 2 |
+
datetime,
|
| 3 |
+
timedelta,
|
| 4 |
+
)
|
| 5 |
+
from io import StringIO
|
| 6 |
+
import re
|
| 7 |
+
import sys
|
| 8 |
+
from textwrap import dedent
|
| 9 |
+
|
| 10 |
+
import numpy as np
|
| 11 |
+
import pytest
|
| 12 |
+
|
| 13 |
+
from pandas._config import using_string_dtype
|
| 14 |
+
|
| 15 |
+
from pandas.errors import Pandas4Warning
|
| 16 |
+
|
| 17 |
+
from pandas import (
|
| 18 |
+
CategoricalIndex,
|
| 19 |
+
DataFrame,
|
| 20 |
+
Index,
|
| 21 |
+
NaT,
|
| 22 |
+
Series,
|
| 23 |
+
Timestamp,
|
| 24 |
+
concat,
|
| 25 |
+
date_range,
|
| 26 |
+
get_option,
|
| 27 |
+
option_context,
|
| 28 |
+
read_csv,
|
| 29 |
+
timedelta_range,
|
| 30 |
+
to_datetime,
|
| 31 |
+
)
|
| 32 |
+
import pandas._testing as tm
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _three_digit_exp():
|
| 36 |
+
return f"{1.7e8:.4g}" == "1.7e+008"
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class TestDataFrameToStringFormatters:
|
| 40 |
+
def test_keyword_deprecation(self):
|
| 41 |
+
# GH 57280
|
| 42 |
+
msg = (
|
| 43 |
+
"Starting with pandas version 4.0 all arguments of to_string "
|
| 44 |
+
"except for the argument 'buf' will be keyword-only."
|
| 45 |
+
)
|
| 46 |
+
s = Series(["a", "b"])
|
| 47 |
+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
|
| 48 |
+
s.to_string(None, "NaN")
|
| 49 |
+
|
| 50 |
+
def test_to_string_masked_ea_with_formatter(self):
|
| 51 |
+
# GH#39336
|
| 52 |
+
df = DataFrame(
|
| 53 |
+
{
|
| 54 |
+
"a": Series([0.123456789, 1.123456789], dtype="Float64"),
|
| 55 |
+
"b": Series([1, 2], dtype="Int64"),
|
| 56 |
+
}
|
| 57 |
+
)
|
| 58 |
+
result = df.to_string(formatters=["{:.2f}".format, "{:.2f}".format])
|
| 59 |
+
expected = dedent(
|
| 60 |
+
"""\
|
| 61 |
+
a b
|
| 62 |
+
0 0.12 1.00
|
| 63 |
+
1 1.12 2.00"""
|
| 64 |
+
)
|
| 65 |
+
assert result == expected
|
| 66 |
+
|
| 67 |
+
def test_to_string_with_formatters(self):
|
| 68 |
+
df = DataFrame(
|
| 69 |
+
{
|
| 70 |
+
"int": [1, 2, 3],
|
| 71 |
+
"float": [1.0, 2.0, 3.0],
|
| 72 |
+
"object": [(1, 2), True, False],
|
| 73 |
+
},
|
| 74 |
+
columns=["int", "float", "object"],
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
formatters = [
|
| 78 |
+
("int", lambda x: f"0x{x:x}"),
|
| 79 |
+
("float", lambda x: f"[{x: 4.1f}]"),
|
| 80 |
+
("object", lambda x: f"-{x!s}-"),
|
| 81 |
+
]
|
| 82 |
+
result = df.to_string(formatters=dict(formatters))
|
| 83 |
+
result2 = df.to_string(formatters=list(zip(*formatters))[1])
|
| 84 |
+
assert result == (
|
| 85 |
+
" int float object\n"
|
| 86 |
+
"0 0x1 [ 1.0] -(1, 2)-\n"
|
| 87 |
+
"1 0x2 [ 2.0] -True-\n"
|
| 88 |
+
"2 0x3 [ 3.0] -False-"
|
| 89 |
+
)
|
| 90 |
+
assert result == result2
|
| 91 |
+
|
| 92 |
+
def test_to_string_with_datetime64_monthformatter(self):
|
| 93 |
+
months = [datetime(2016, 1, 1), datetime(2016, 2, 2)]
|
| 94 |
+
x = DataFrame({"months": months})
|
| 95 |
+
|
| 96 |
+
def format_func(x):
|
| 97 |
+
return x.strftime("%Y-%m")
|
| 98 |
+
|
| 99 |
+
result = x.to_string(formatters={"months": format_func})
|
| 100 |
+
expected = dedent(
|
| 101 |
+
"""\
|
| 102 |
+
months
|
| 103 |
+
0 2016-01
|
| 104 |
+
1 2016-02"""
|
| 105 |
+
)
|
| 106 |
+
assert result.strip() == expected
|
| 107 |
+
|
| 108 |
+
def test_to_string_with_datetime64_hourformatter(self):
|
| 109 |
+
x = DataFrame(
|
| 110 |
+
{"hod": to_datetime(["10:10:10.100", "12:12:12.120"], format="%H:%M:%S.%f")}
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
def format_func(x):
|
| 114 |
+
return x.strftime("%H:%M")
|
| 115 |
+
|
| 116 |
+
result = x.to_string(formatters={"hod": format_func})
|
| 117 |
+
expected = dedent(
|
| 118 |
+
"""\
|
| 119 |
+
hod
|
| 120 |
+
0 10:10
|
| 121 |
+
1 12:12"""
|
| 122 |
+
)
|
| 123 |
+
assert result.strip() == expected
|
| 124 |
+
|
| 125 |
+
def test_to_string_with_formatters_unicode(self):
|
| 126 |
+
df = DataFrame({"c/\u03c3": [1, 2, 3]})
|
| 127 |
+
result = df.to_string(formatters={"c/\u03c3": str})
|
| 128 |
+
expected = dedent(
|
| 129 |
+
"""\
|
| 130 |
+
c/\u03c3
|
| 131 |
+
0 1
|
| 132 |
+
1 2
|
| 133 |
+
2 3"""
|
| 134 |
+
)
|
| 135 |
+
assert result == expected
|
| 136 |
+
|
| 137 |
+
def test_to_string_index_formatter(self):
|
| 138 |
+
df = DataFrame([range(5), range(5, 10), range(10, 15)])
|
| 139 |
+
rs = df.to_string(formatters={"__index__": lambda x: "abc"[x]})
|
| 140 |
+
xp = dedent(
|
| 141 |
+
"""\
|
| 142 |
+
0 1 2 3 4
|
| 143 |
+
a 0 1 2 3 4
|
| 144 |
+
b 5 6 7 8 9
|
| 145 |
+
c 10 11 12 13 14"""
|
| 146 |
+
)
|
| 147 |
+
assert rs == xp
|
| 148 |
+
|
| 149 |
+
def test_no_extra_space(self):
|
| 150 |
+
# GH#52690: Check that no extra space is given
|
| 151 |
+
col1 = "TEST"
|
| 152 |
+
col2 = "PANDAS"
|
| 153 |
+
col3 = "to_string"
|
| 154 |
+
expected = f"{col1:<6s} {col2:<7s} {col3:<10s}"
|
| 155 |
+
df = DataFrame([{"col1": "TEST", "col2": "PANDAS", "col3": "to_string"}])
|
| 156 |
+
d = {"col1": "{:<6s}".format, "col2": "{:<7s}".format, "col3": "{:<10s}".format}
|
| 157 |
+
result = df.to_string(index=False, header=False, formatters=d)
|
| 158 |
+
assert result == expected
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
class TestDataFrameToStringColSpace:
|
| 162 |
+
def test_to_string_with_column_specific_col_space_raises(self):
|
| 163 |
+
df = DataFrame(
|
| 164 |
+
np.random.default_rng(2).random(size=(3, 3)), columns=["a", "b", "c"]
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
msg = (
|
| 168 |
+
"Col_space length\\(\\d+\\) should match "
|
| 169 |
+
"DataFrame number of columns\\(\\d+\\)"
|
| 170 |
+
)
|
| 171 |
+
with pytest.raises(ValueError, match=msg):
|
| 172 |
+
df.to_string(col_space=[30, 40])
|
| 173 |
+
|
| 174 |
+
with pytest.raises(ValueError, match=msg):
|
| 175 |
+
df.to_string(col_space=[30, 40, 50, 60])
|
| 176 |
+
|
| 177 |
+
msg = "unknown column"
|
| 178 |
+
with pytest.raises(ValueError, match=msg):
|
| 179 |
+
df.to_string(col_space={"a": "foo", "b": 23, "d": 34})
|
| 180 |
+
|
| 181 |
+
def test_to_string_with_column_specific_col_space(self):
|
| 182 |
+
df = DataFrame(
|
| 183 |
+
np.random.default_rng(2).random(size=(3, 3)), columns=["a", "b", "c"]
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
result = df.to_string(col_space={"a": 10, "b": 11, "c": 12})
|
| 187 |
+
# 3 separating space + each col_space for (id, a, b, c)
|
| 188 |
+
assert len(result.split("\n")[1]) == (3 + 1 + 10 + 11 + 12)
|
| 189 |
+
|
| 190 |
+
result = df.to_string(col_space=[10, 11, 12])
|
| 191 |
+
assert len(result.split("\n")[1]) == (3 + 1 + 10 + 11 + 12)
|
| 192 |
+
|
| 193 |
+
def test_to_string_with_col_space(self):
|
| 194 |
+
df = DataFrame(np.random.default_rng(2).random(size=(1, 3)))
|
| 195 |
+
c10 = len(df.to_string(col_space=10).split("\n")[1])
|
| 196 |
+
c20 = len(df.to_string(col_space=20).split("\n")[1])
|
| 197 |
+
c30 = len(df.to_string(col_space=30).split("\n")[1])
|
| 198 |
+
assert c10 < c20 < c30
|
| 199 |
+
|
| 200 |
+
# GH#8230
|
| 201 |
+
# col_space wasn't being applied with header=False
|
| 202 |
+
with_header = df.to_string(col_space=20)
|
| 203 |
+
with_header_row1 = with_header.splitlines()[1]
|
| 204 |
+
no_header = df.to_string(col_space=20, header=False)
|
| 205 |
+
assert len(with_header_row1) == len(no_header)
|
| 206 |
+
|
| 207 |
+
def test_to_string_repr_tuples(self):
|
| 208 |
+
buf = StringIO()
|
| 209 |
+
|
| 210 |
+
df = DataFrame({"tups": list(zip(range(10), range(10)))})
|
| 211 |
+
repr(df)
|
| 212 |
+
df.to_string(col_space=10, buf=buf)
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
class TestDataFrameToStringHeader:
|
| 216 |
+
def test_to_string_header_false(self):
|
| 217 |
+
# GH#49230
|
| 218 |
+
df = DataFrame([1, 2])
|
| 219 |
+
df.index.name = "a"
|
| 220 |
+
s = df.to_string(header=False)
|
| 221 |
+
expected = "a \n0 1\n1 2"
|
| 222 |
+
assert s == expected
|
| 223 |
+
|
| 224 |
+
df = DataFrame([[1, 2], [3, 4]])
|
| 225 |
+
df.index.name = "a"
|
| 226 |
+
s = df.to_string(header=False)
|
| 227 |
+
expected = "a \n0 1 2\n1 3 4"
|
| 228 |
+
assert s == expected
|
| 229 |
+
|
| 230 |
+
def test_to_string_multindex_header(self):
|
| 231 |
+
# GH#16718
|
| 232 |
+
df = DataFrame({"a": [0], "b": [1], "c": [2], "d": [3]}).set_index(["a", "b"])
|
| 233 |
+
res = df.to_string(header=["r1", "r2"])
|
| 234 |
+
exp = " r1 r2\na b \n0 1 2 3"
|
| 235 |
+
assert res == exp
|
| 236 |
+
|
| 237 |
+
def test_to_string_no_header(self):
|
| 238 |
+
df = DataFrame({"x": [1, 2, 3], "y": [4, 5, 6]})
|
| 239 |
+
|
| 240 |
+
df_s = df.to_string(header=False)
|
| 241 |
+
expected = "0 1 4\n1 2 5\n2 3 6"
|
| 242 |
+
|
| 243 |
+
assert df_s == expected
|
| 244 |
+
|
| 245 |
+
def test_to_string_specified_header(self):
|
| 246 |
+
df = DataFrame({"x": [1, 2, 3], "y": [4, 5, 6]})
|
| 247 |
+
|
| 248 |
+
df_s = df.to_string(header=["X", "Y"])
|
| 249 |
+
expected = " X Y\n0 1 4\n1 2 5\n2 3 6"
|
| 250 |
+
|
| 251 |
+
assert df_s == expected
|
| 252 |
+
|
| 253 |
+
msg = "Writing 2 cols but got 1 aliases"
|
| 254 |
+
with pytest.raises(ValueError, match=msg):
|
| 255 |
+
df.to_string(header=["X"])
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
class TestDataFrameToStringLineWidth:
|
| 259 |
+
def test_to_string_line_width(self):
|
| 260 |
+
df = DataFrame(123, index=range(10, 15), columns=range(30))
|
| 261 |
+
lines = df.to_string(line_width=80)
|
| 262 |
+
assert max(len(line) for line in lines.split("\n")) == 80
|
| 263 |
+
|
| 264 |
+
def test_to_string_line_width_no_index(self):
|
| 265 |
+
# GH#13998, GH#22505
|
| 266 |
+
df = DataFrame({"x": [1, 2, 3], "y": [4, 5, 6]})
|
| 267 |
+
|
| 268 |
+
df_s = df.to_string(line_width=1, index=False)
|
| 269 |
+
expected = " x \\\n 1 \n 2 \n 3 \n\n y \n 4 \n 5 \n 6 "
|
| 270 |
+
|
| 271 |
+
assert df_s == expected
|
| 272 |
+
|
| 273 |
+
df = DataFrame({"x": [11, 22, 33], "y": [4, 5, 6]})
|
| 274 |
+
|
| 275 |
+
df_s = df.to_string(line_width=1, index=False)
|
| 276 |
+
expected = " x \\\n11 \n22 \n33 \n\n y \n 4 \n 5 \n 6 "
|
| 277 |
+
|
| 278 |
+
assert df_s == expected
|
| 279 |
+
|
| 280 |
+
df = DataFrame({"x": [11, 22, -33], "y": [4, 5, -6]})
|
| 281 |
+
|
| 282 |
+
df_s = df.to_string(line_width=1, index=False)
|
| 283 |
+
expected = " x \\\n 11 \n 22 \n-33 \n\n y \n 4 \n 5 \n-6 "
|
| 284 |
+
|
| 285 |
+
assert df_s == expected
|
| 286 |
+
|
| 287 |
+
def test_to_string_line_width_no_header(self):
|
| 288 |
+
# GH#53054
|
| 289 |
+
df = DataFrame({"x": [1, 2, 3], "y": [4, 5, 6]})
|
| 290 |
+
|
| 291 |
+
df_s = df.to_string(line_width=1, header=False)
|
| 292 |
+
expected = "0 1 \\\n1 2 \n2 3 \n\n0 4 \n1 5 \n2 6 "
|
| 293 |
+
|
| 294 |
+
assert df_s == expected
|
| 295 |
+
|
| 296 |
+
df = DataFrame({"x": [11, 22, 33], "y": [4, 5, 6]})
|
| 297 |
+
|
| 298 |
+
df_s = df.to_string(line_width=1, header=False)
|
| 299 |
+
expected = "0 11 \\\n1 22 \n2 33 \n\n0 4 \n1 5 \n2 6 "
|
| 300 |
+
|
| 301 |
+
assert df_s == expected
|
| 302 |
+
|
| 303 |
+
df = DataFrame({"x": [11, 22, -33], "y": [4, 5, -6]})
|
| 304 |
+
|
| 305 |
+
df_s = df.to_string(line_width=1, header=False)
|
| 306 |
+
expected = "0 11 \\\n1 22 \n2 -33 \n\n0 4 \n1 5 \n2 -6 "
|
| 307 |
+
|
| 308 |
+
assert df_s == expected
|
| 309 |
+
|
| 310 |
+
def test_to_string_line_width_with_both_index_and_header(self):
|
| 311 |
+
# GH#53054
|
| 312 |
+
df = DataFrame({"x": [1, 2, 3], "y": [4, 5, 6]})
|
| 313 |
+
|
| 314 |
+
df_s = df.to_string(line_width=1)
|
| 315 |
+
expected = (
|
| 316 |
+
" x \\\n0 1 \n1 2 \n2 3 \n\n y \n0 4 \n1 5 \n2 6 "
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
assert df_s == expected
|
| 320 |
+
|
| 321 |
+
df = DataFrame({"x": [11, 22, 33], "y": [4, 5, 6]})
|
| 322 |
+
|
| 323 |
+
df_s = df.to_string(line_width=1)
|
| 324 |
+
expected = (
|
| 325 |
+
" x \\\n0 11 \n1 22 \n2 33 \n\n y \n0 4 \n1 5 \n2 6 "
|
| 326 |
+
)
|
| 327 |
+
|
| 328 |
+
assert df_s == expected
|
| 329 |
+
|
| 330 |
+
df = DataFrame({"x": [11, 22, -33], "y": [4, 5, -6]})
|
| 331 |
+
|
| 332 |
+
df_s = df.to_string(line_width=1)
|
| 333 |
+
expected = (
|
| 334 |
+
" x \\\n0 11 \n1 22 \n2 -33 \n\n y \n0 4 \n1 5 \n2 -6 "
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
assert df_s == expected
|
| 338 |
+
|
| 339 |
+
def test_to_string_line_width_no_index_no_header(self):
|
| 340 |
+
# GH#53054
|
| 341 |
+
df = DataFrame({"x": [1, 2, 3], "y": [4, 5, 6]})
|
| 342 |
+
|
| 343 |
+
df_s = df.to_string(line_width=1, index=False, header=False)
|
| 344 |
+
expected = "1 \\\n2 \n3 \n\n4 \n5 \n6 "
|
| 345 |
+
|
| 346 |
+
assert df_s == expected
|
| 347 |
+
|
| 348 |
+
df = DataFrame({"x": [11, 22, 33], "y": [4, 5, 6]})
|
| 349 |
+
|
| 350 |
+
df_s = df.to_string(line_width=1, index=False, header=False)
|
| 351 |
+
expected = "11 \\\n22 \n33 \n\n4 \n5 \n6 "
|
| 352 |
+
|
| 353 |
+
assert df_s == expected
|
| 354 |
+
|
| 355 |
+
df = DataFrame({"x": [11, 22, -33], "y": [4, 5, -6]})
|
| 356 |
+
|
| 357 |
+
df_s = df.to_string(line_width=1, index=False, header=False)
|
| 358 |
+
expected = " 11 \\\n 22 \n-33 \n\n 4 \n 5 \n-6 "
|
| 359 |
+
|
| 360 |
+
assert df_s == expected
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
class TestToStringNumericFormatting:
|
| 364 |
+
def test_to_string_float_format_no_fixed_width(self):
|
| 365 |
+
# GH#21625
|
| 366 |
+
df = DataFrame({"x": [0.19999]})
|
| 367 |
+
expected = " x\n0 0.200"
|
| 368 |
+
assert df.to_string(float_format="%.3f") == expected
|
| 369 |
+
|
| 370 |
+
# GH#22270
|
| 371 |
+
df = DataFrame({"x": [100.0]})
|
| 372 |
+
expected = " x\n0 100"
|
| 373 |
+
assert df.to_string(float_format="%.0f") == expected
|
| 374 |
+
|
| 375 |
+
def test_to_string_small_float_values(self):
|
| 376 |
+
df = DataFrame({"a": [1.5, 1e-17, -5.5e-7]})
|
| 377 |
+
|
| 378 |
+
result = df.to_string()
|
| 379 |
+
# sadness per above
|
| 380 |
+
if _three_digit_exp():
|
| 381 |
+
expected = (
|
| 382 |
+
" a\n0 1.500000e+000\n1 1.000000e-017\n2 -5.500000e-007"
|
| 383 |
+
)
|
| 384 |
+
else:
|
| 385 |
+
expected = (
|
| 386 |
+
" a\n0 1.500000e+00\n1 1.000000e-17\n2 -5.500000e-07"
|
| 387 |
+
)
|
| 388 |
+
assert result == expected
|
| 389 |
+
|
| 390 |
+
# but not all exactly zero
|
| 391 |
+
df = df * 0
|
| 392 |
+
result = df.to_string()
|
| 393 |
+
expected = " 0\n0 0\n1 0\n2 -0"
|
| 394 |
+
# TODO: assert that these match??
|
| 395 |
+
|
| 396 |
+
def test_to_string_complex_float_formatting(self):
|
| 397 |
+
# GH #25514, 25745
|
| 398 |
+
with option_context("display.precision", 5):
|
| 399 |
+
df = DataFrame(
|
| 400 |
+
{
|
| 401 |
+
"x": [
|
| 402 |
+
(0.4467846931321966 + 0.0715185102060818j),
|
| 403 |
+
(0.2739442392974528 + 0.23515228785438969j),
|
| 404 |
+
(0.26974928742135185 + 0.3250604054898979j),
|
| 405 |
+
(-1j),
|
| 406 |
+
]
|
| 407 |
+
}
|
| 408 |
+
)
|
| 409 |
+
result = df.to_string()
|
| 410 |
+
expected = (
|
| 411 |
+
" x\n0 0.44678+0.07152j\n"
|
| 412 |
+
"1 0.27394+0.23515j\n"
|
| 413 |
+
"2 0.26975+0.32506j\n"
|
| 414 |
+
"3 -0.00000-1.00000j"
|
| 415 |
+
)
|
| 416 |
+
assert result == expected
|
| 417 |
+
|
| 418 |
+
def test_to_string_complex_float_formatting_with_exponents(self):
|
| 419 |
+
# GH #60393
|
| 420 |
+
with option_context("display.precision", 6):
|
| 421 |
+
df = DataFrame(
|
| 422 |
+
{
|
| 423 |
+
"x": [
|
| 424 |
+
(1.8816e-09 + 0j),
|
| 425 |
+
(1.8816e-09 + 3.39676e-09j),
|
| 426 |
+
]
|
| 427 |
+
}
|
| 428 |
+
)
|
| 429 |
+
result = df.to_string()
|
| 430 |
+
expected = (
|
| 431 |
+
" x\n0 1.881600e-09+0.000000e+00j\n"
|
| 432 |
+
"1 1.881600e-09+3.396760e-09j"
|
| 433 |
+
)
|
| 434 |
+
assert result == expected
|
| 435 |
+
|
| 436 |
+
def test_to_string_format_inf(self):
|
| 437 |
+
# GH#24861
|
| 438 |
+
df = DataFrame(
|
| 439 |
+
{
|
| 440 |
+
"A": [-np.inf, np.inf, -1, -2.1234, 3, 4],
|
| 441 |
+
"B": [-np.inf, np.inf, "foo", "foooo", "fooooo", "bar"],
|
| 442 |
+
}
|
| 443 |
+
)
|
| 444 |
+
result = df.to_string()
|
| 445 |
+
|
| 446 |
+
expected = (
|
| 447 |
+
" A B\n"
|
| 448 |
+
"0 -inf -inf\n"
|
| 449 |
+
"1 inf inf\n"
|
| 450 |
+
"2 -1.0000 foo\n"
|
| 451 |
+
"3 -2.1234 foooo\n"
|
| 452 |
+
"4 3.0000 fooooo\n"
|
| 453 |
+
"5 4.0000 bar"
|
| 454 |
+
)
|
| 455 |
+
assert result == expected
|
| 456 |
+
|
| 457 |
+
df = DataFrame(
|
| 458 |
+
{
|
| 459 |
+
"A": [-np.inf, np.inf, -1.0, -2.0, 3.0, 4.0],
|
| 460 |
+
"B": [-np.inf, np.inf, "foo", "foooo", "fooooo", "bar"],
|
| 461 |
+
}
|
| 462 |
+
)
|
| 463 |
+
result = df.to_string()
|
| 464 |
+
|
| 465 |
+
expected = (
|
| 466 |
+
" A B\n"
|
| 467 |
+
"0 -inf -inf\n"
|
| 468 |
+
"1 inf inf\n"
|
| 469 |
+
"2 -1.0 foo\n"
|
| 470 |
+
"3 -2.0 foooo\n"
|
| 471 |
+
"4 3.0 fooooo\n"
|
| 472 |
+
"5 4.0 bar"
|
| 473 |
+
)
|
| 474 |
+
assert result == expected
|
| 475 |
+
|
| 476 |
+
def test_to_string_int_formatting(self):
|
| 477 |
+
df = DataFrame({"x": [-15, 20, 25, -35]})
|
| 478 |
+
assert issubclass(df["x"].dtype.type, np.integer)
|
| 479 |
+
|
| 480 |
+
output = df.to_string()
|
| 481 |
+
expected = " x\n0 -15\n1 20\n2 25\n3 -35"
|
| 482 |
+
assert output == expected
|
| 483 |
+
|
| 484 |
+
def test_to_string_float_formatting(self):
|
| 485 |
+
with option_context(
|
| 486 |
+
"display.precision",
|
| 487 |
+
5,
|
| 488 |
+
"display.notebook_repr_html",
|
| 489 |
+
False,
|
| 490 |
+
):
|
| 491 |
+
df = DataFrame(
|
| 492 |
+
{"x": [0, 0.25, 3456.000, 12e45, 1.64e6, 1.7e8, 1.253456, np.pi, -1e6]}
|
| 493 |
+
)
|
| 494 |
+
|
| 495 |
+
df_s = df.to_string()
|
| 496 |
+
|
| 497 |
+
if _three_digit_exp():
|
| 498 |
+
expected = (
|
| 499 |
+
" x\n0 0.00000e+000\n1 2.50000e-001\n"
|
| 500 |
+
"2 3.45600e+003\n3 1.20000e+046\n4 1.64000e+006\n"
|
| 501 |
+
"5 1.70000e+008\n6 1.25346e+000\n7 3.14159e+000\n"
|
| 502 |
+
"8 -1.00000e+006"
|
| 503 |
+
)
|
| 504 |
+
else:
|
| 505 |
+
expected = (
|
| 506 |
+
" x\n0 0.00000e+00\n1 2.50000e-01\n"
|
| 507 |
+
"2 3.45600e+03\n3 1.20000e+46\n4 1.64000e+06\n"
|
| 508 |
+
"5 1.70000e+08\n6 1.25346e+00\n7 3.14159e+00\n"
|
| 509 |
+
"8 -1.00000e+06"
|
| 510 |
+
)
|
| 511 |
+
assert df_s == expected
|
| 512 |
+
|
| 513 |
+
df = DataFrame({"x": [3234, 0.253]})
|
| 514 |
+
df_s = df.to_string()
|
| 515 |
+
|
| 516 |
+
expected = " x\n0 3234.000\n1 0.253"
|
| 517 |
+
assert df_s == expected
|
| 518 |
+
|
| 519 |
+
assert get_option("display.precision") == 6
|
| 520 |
+
|
| 521 |
+
df = DataFrame({"x": [1e9, 0.2512]})
|
| 522 |
+
df_s = df.to_string()
|
| 523 |
+
|
| 524 |
+
if _three_digit_exp():
|
| 525 |
+
expected = " x\n0 1.000000e+009\n1 2.512000e-001"
|
| 526 |
+
else:
|
| 527 |
+
expected = " x\n0 1.000000e+09\n1 2.512000e-01"
|
| 528 |
+
assert df_s == expected
|
| 529 |
+
|
| 530 |
+
|
| 531 |
+
class TestDataFrameToString:
|
| 532 |
+
def test_to_string_decimal(self):
|
| 533 |
+
# GH#23614
|
| 534 |
+
df = DataFrame({"A": [6.0, 3.1, 2.2]})
|
| 535 |
+
expected = " A\n0 6,0\n1 3,1\n2 2,2"
|
| 536 |
+
assert df.to_string(decimal=",") == expected
|
| 537 |
+
|
| 538 |
+
def test_to_string_left_justify_cols(self):
|
| 539 |
+
df = DataFrame({"x": [3234, 0.253]})
|
| 540 |
+
df_s = df.to_string(justify="left")
|
| 541 |
+
expected = " x \n0 3234.000\n1 0.253"
|
| 542 |
+
assert df_s == expected
|
| 543 |
+
|
| 544 |
+
def test_to_string_format_na(self):
|
| 545 |
+
df = DataFrame(
|
| 546 |
+
{
|
| 547 |
+
"A": [np.nan, -1, -2.1234, 3, 4],
|
| 548 |
+
"B": [np.nan, "foo", "foooo", "fooooo", "bar"],
|
| 549 |
+
}
|
| 550 |
+
)
|
| 551 |
+
result = df.to_string()
|
| 552 |
+
|
| 553 |
+
expected = (
|
| 554 |
+
" A B\n"
|
| 555 |
+
"0 NaN NaN\n"
|
| 556 |
+
"1 -1.0000 foo\n"
|
| 557 |
+
"2 -2.1234 foooo\n"
|
| 558 |
+
"3 3.0000 fooooo\n"
|
| 559 |
+
"4 4.0000 bar"
|
| 560 |
+
)
|
| 561 |
+
assert result == expected
|
| 562 |
+
|
| 563 |
+
df = DataFrame(
|
| 564 |
+
{
|
| 565 |
+
"A": [np.nan, -1.0, -2.0, 3.0, 4.0],
|
| 566 |
+
"B": [np.nan, "foo", "foooo", "fooooo", "bar"],
|
| 567 |
+
}
|
| 568 |
+
)
|
| 569 |
+
result = df.to_string()
|
| 570 |
+
|
| 571 |
+
expected = (
|
| 572 |
+
" A B\n"
|
| 573 |
+
"0 NaN NaN\n"
|
| 574 |
+
"1 -1.0 foo\n"
|
| 575 |
+
"2 -2.0 foooo\n"
|
| 576 |
+
"3 3.0 fooooo\n"
|
| 577 |
+
"4 4.0 bar"
|
| 578 |
+
)
|
| 579 |
+
assert result == expected
|
| 580 |
+
|
| 581 |
+
def test_to_string_with_dict_entries(self):
|
| 582 |
+
df = DataFrame({"A": [{"a": 1, "b": 2}]})
|
| 583 |
+
|
| 584 |
+
val = df.to_string()
|
| 585 |
+
assert "'a': 1" in val
|
| 586 |
+
assert "'b': 2" in val
|
| 587 |
+
|
| 588 |
+
def test_to_string_with_categorical_columns(self):
|
| 589 |
+
# GH#35439
|
| 590 |
+
data = [[4, 2], [3, 2], [4, 3]]
|
| 591 |
+
cols = ["aaaaaaaaa", "b"]
|
| 592 |
+
df = DataFrame(data, columns=cols)
|
| 593 |
+
df_cat_cols = DataFrame(data, columns=CategoricalIndex(cols))
|
| 594 |
+
|
| 595 |
+
assert df.to_string() == df_cat_cols.to_string()
|
| 596 |
+
|
| 597 |
+
def test_repr_embedded_ndarray(self):
|
| 598 |
+
arr = np.empty(10, dtype=[("err", object)])
|
| 599 |
+
for i in range(len(arr)):
|
| 600 |
+
arr["err"][i] = np.random.default_rng(2).standard_normal(i)
|
| 601 |
+
|
| 602 |
+
df = DataFrame(arr)
|
| 603 |
+
repr(df["err"])
|
| 604 |
+
repr(df)
|
| 605 |
+
df.to_string()
|
| 606 |
+
|
| 607 |
+
def test_to_string_truncate(self):
|
| 608 |
+
# GH 9784 - dont truncate when calling DataFrame.to_string
|
| 609 |
+
df = DataFrame(
|
| 610 |
+
[
|
| 611 |
+
{
|
| 612 |
+
"a": "foo",
|
| 613 |
+
"b": "bar",
|
| 614 |
+
"c": "let's make this a very VERY long line that is longer "
|
| 615 |
+
"than the default 50 character limit",
|
| 616 |
+
"d": 1,
|
| 617 |
+
},
|
| 618 |
+
{"a": "foo", "b": "bar", "c": "stuff", "d": 1},
|
| 619 |
+
]
|
| 620 |
+
)
|
| 621 |
+
df.set_index(["a", "b", "c"])
|
| 622 |
+
assert df.to_string() == (
|
| 623 |
+
" a b "
|
| 624 |
+
" c d\n"
|
| 625 |
+
"0 foo bar let's make this a very VERY long line t"
|
| 626 |
+
"hat is longer than the default 50 character limit 1\n"
|
| 627 |
+
"1 foo bar "
|
| 628 |
+
" stuff 1"
|
| 629 |
+
)
|
| 630 |
+
with option_context("max_colwidth", 20):
|
| 631 |
+
# the display option has no effect on the to_string method
|
| 632 |
+
assert df.to_string() == (
|
| 633 |
+
" a b "
|
| 634 |
+
" c d\n"
|
| 635 |
+
"0 foo bar let's make this a very VERY long line t"
|
| 636 |
+
"hat is longer than the default 50 character limit 1\n"
|
| 637 |
+
"1 foo bar "
|
| 638 |
+
" stuff 1"
|
| 639 |
+
)
|
| 640 |
+
assert df.to_string(max_colwidth=20) == (
|
| 641 |
+
" a b c d\n"
|
| 642 |
+
"0 foo bar let's make this ... 1\n"
|
| 643 |
+
"1 foo bar stuff 1"
|
| 644 |
+
)
|
| 645 |
+
|
| 646 |
+
@pytest.mark.parametrize(
|
| 647 |
+
"input_array, expected",
|
| 648 |
+
[
|
| 649 |
+
({"A": ["a"]}, "A\na"),
|
| 650 |
+
({"A": ["a", "b"], "B": ["c", "dd"]}, "A B\na c\nb dd"),
|
| 651 |
+
({"A": ["a", 1], "B": ["aa", 1]}, "A B\na aa\n1 1"),
|
| 652 |
+
],
|
| 653 |
+
)
|
| 654 |
+
def test_format_remove_leading_space_dataframe(self, input_array, expected):
|
| 655 |
+
# GH#24980
|
| 656 |
+
df = DataFrame(input_array).to_string(index=False)
|
| 657 |
+
assert df == expected
|
| 658 |
+
|
| 659 |
+
@pytest.mark.parametrize(
|
| 660 |
+
"data,expected",
|
| 661 |
+
[
|
| 662 |
+
(
|
| 663 |
+
{"col1": [1, 2], "col2": [3, 4]},
|
| 664 |
+
" col1 col2\n0 1 3\n1 2 4",
|
| 665 |
+
),
|
| 666 |
+
(
|
| 667 |
+
{"col1": ["Abc", 0.756], "col2": [np.nan, 4.5435]},
|
| 668 |
+
" col1 col2\n0 Abc NaN\n1 0.756 4.5435",
|
| 669 |
+
),
|
| 670 |
+
(
|
| 671 |
+
{"col1": [np.nan, "a"], "col2": [0.009, 3.543], "col3": ["Abc", 23]},
|
| 672 |
+
" col1 col2 col3\n0 NaN 0.009 Abc\n1 a 3.543 23",
|
| 673 |
+
),
|
| 674 |
+
],
|
| 675 |
+
)
|
| 676 |
+
def test_to_string_max_rows_zero(self, data, expected):
|
| 677 |
+
# GH#35394
|
| 678 |
+
result = DataFrame(data=data).to_string(max_rows=0)
|
| 679 |
+
assert result == expected
|
| 680 |
+
|
| 681 |
+
@pytest.mark.parametrize(
|
| 682 |
+
"max_cols, max_rows, expected",
|
| 683 |
+
[
|
| 684 |
+
(
|
| 685 |
+
10,
|
| 686 |
+
None,
|
| 687 |
+
" 0 1 2 3 4 ... 6 7 8 9 10\n"
|
| 688 |
+
" 0 0 0 0 0 ... 0 0 0 0 0\n"
|
| 689 |
+
" 0 0 0 0 0 ... 0 0 0 0 0\n"
|
| 690 |
+
" 0 0 0 0 0 ... 0 0 0 0 0\n"
|
| 691 |
+
" 0 0 0 0 0 ... 0 0 0 0 0",
|
| 692 |
+
),
|
| 693 |
+
(
|
| 694 |
+
None,
|
| 695 |
+
2,
|
| 696 |
+
" 0 1 2 3 4 5 6 7 8 9 10\n"
|
| 697 |
+
" 0 0 0 0 0 0 0 0 0 0 0\n"
|
| 698 |
+
" .. .. .. .. .. .. .. .. .. .. ..\n"
|
| 699 |
+
" 0 0 0 0 0 0 0 0 0 0 0",
|
| 700 |
+
),
|
| 701 |
+
(
|
| 702 |
+
10,
|
| 703 |
+
2,
|
| 704 |
+
" 0 1 2 3 4 ... 6 7 8 9 10\n"
|
| 705 |
+
" 0 0 0 0 0 ... 0 0 0 0 0\n"
|
| 706 |
+
" .. .. .. .. .. ... .. .. .. .. ..\n"
|
| 707 |
+
" 0 0 0 0 0 ... 0 0 0 0 0",
|
| 708 |
+
),
|
| 709 |
+
(
|
| 710 |
+
9,
|
| 711 |
+
2,
|
| 712 |
+
" 0 1 2 3 ... 7 8 9 10\n"
|
| 713 |
+
" 0 0 0 0 ... 0 0 0 0\n"
|
| 714 |
+
" .. .. .. .. ... .. .. .. ..\n"
|
| 715 |
+
" 0 0 0 0 ... 0 0 0 0",
|
| 716 |
+
),
|
| 717 |
+
(
|
| 718 |
+
1,
|
| 719 |
+
1,
|
| 720 |
+
" 0 ...\n 0 ...\n.. ...",
|
| 721 |
+
),
|
| 722 |
+
],
|
| 723 |
+
)
|
| 724 |
+
def test_truncation_no_index(self, max_cols, max_rows, expected):
|
| 725 |
+
df = DataFrame([[0] * 11] * 4)
|
| 726 |
+
assert (
|
| 727 |
+
df.to_string(index=False, max_cols=max_cols, max_rows=max_rows) == expected
|
| 728 |
+
)
|
| 729 |
+
|
| 730 |
+
def test_to_string_no_index(self):
|
| 731 |
+
# GH#16839, GH#13032
|
| 732 |
+
df = DataFrame({"x": [11, 22], "y": [33, -44], "z": ["AAA", " "]})
|
| 733 |
+
|
| 734 |
+
df_s = df.to_string(index=False)
|
| 735 |
+
# Leading space is expected for positive numbers.
|
| 736 |
+
expected = " x y z\n11 33 AAA\n22 -44 "
|
| 737 |
+
assert df_s == expected
|
| 738 |
+
|
| 739 |
+
df_s = df[["y", "x", "z"]].to_string(index=False)
|
| 740 |
+
expected = " y x z\n 33 11 AAA\n-44 22 "
|
| 741 |
+
assert df_s == expected
|
| 742 |
+
|
| 743 |
+
def test_to_string_unicode_columns(self, float_frame):
|
| 744 |
+
df = DataFrame({"\u03c3": np.arange(10.0)})
|
| 745 |
+
|
| 746 |
+
buf = StringIO()
|
| 747 |
+
df.to_string(buf=buf)
|
| 748 |
+
buf.getvalue()
|
| 749 |
+
|
| 750 |
+
buf = StringIO()
|
| 751 |
+
df.info(buf=buf)
|
| 752 |
+
buf.getvalue()
|
| 753 |
+
|
| 754 |
+
result = float_frame.to_string()
|
| 755 |
+
assert isinstance(result, str)
|
| 756 |
+
|
| 757 |
+
@pytest.mark.parametrize("na_rep", ["NaN", "Ted"])
|
| 758 |
+
def test_to_string_na_rep_and_float_format(self, na_rep):
|
| 759 |
+
# GH#13828
|
| 760 |
+
df = DataFrame([["A", 1.2225], ["A", None]], columns=["Group", "Data"])
|
| 761 |
+
result = df.to_string(na_rep=na_rep, float_format="{:.2f}".format)
|
| 762 |
+
expected = dedent(
|
| 763 |
+
f"""\
|
| 764 |
+
Group Data
|
| 765 |
+
0 A 1.22
|
| 766 |
+
1 A {na_rep}"""
|
| 767 |
+
)
|
| 768 |
+
assert result == expected
|
| 769 |
+
|
| 770 |
+
def test_to_string_string_dtype(self):
|
| 771 |
+
# GH#50099
|
| 772 |
+
pytest.importorskip("pyarrow")
|
| 773 |
+
df = DataFrame(
|
| 774 |
+
{"x": ["foo", "bar", "baz"], "y": ["a", "b", "c"], "z": [1, 2, 3]}
|
| 775 |
+
)
|
| 776 |
+
df = df.astype(
|
| 777 |
+
{"x": "string[pyarrow]", "y": "string[python]", "z": "int64[pyarrow]"}
|
| 778 |
+
)
|
| 779 |
+
result = df.dtypes.to_string()
|
| 780 |
+
expected = dedent(
|
| 781 |
+
"""\
|
| 782 |
+
x string
|
| 783 |
+
y string
|
| 784 |
+
z int64[pyarrow]"""
|
| 785 |
+
)
|
| 786 |
+
assert result == expected
|
| 787 |
+
|
| 788 |
+
def test_to_string_utf8_columns(self):
|
| 789 |
+
n = "\u05d0".encode()
|
| 790 |
+
df = DataFrame([1, 2], columns=[n])
|
| 791 |
+
|
| 792 |
+
with option_context("display.max_rows", 1):
|
| 793 |
+
repr(df)
|
| 794 |
+
|
| 795 |
+
def test_to_string_unicode_two(self):
|
| 796 |
+
dm = DataFrame({"c/\u03c3": []})
|
| 797 |
+
buf = StringIO()
|
| 798 |
+
dm.to_string(buf)
|
| 799 |
+
|
| 800 |
+
def test_to_string_unicode_three(self):
|
| 801 |
+
dm = DataFrame(["\xc2"])
|
| 802 |
+
buf = StringIO()
|
| 803 |
+
dm.to_string(buf)
|
| 804 |
+
|
| 805 |
+
def test_to_string_with_float_index(self):
|
| 806 |
+
index = Index([1.5, 2, 3, 4, 5])
|
| 807 |
+
df = DataFrame(np.arange(5), index=index)
|
| 808 |
+
|
| 809 |
+
result = df.to_string()
|
| 810 |
+
expected = " 0\n1.5 0\n2.0 1\n3.0 2\n4.0 3\n5.0 4"
|
| 811 |
+
assert result == expected
|
| 812 |
+
|
| 813 |
+
def test_to_string(self):
|
| 814 |
+
# big mixed
|
| 815 |
+
biggie = DataFrame(
|
| 816 |
+
{
|
| 817 |
+
"A": np.random.default_rng(2).standard_normal(200),
|
| 818 |
+
"B": Index([f"{i}?!" for i in range(200)]),
|
| 819 |
+
},
|
| 820 |
+
)
|
| 821 |
+
|
| 822 |
+
biggie.loc[:20, "A"] = np.nan
|
| 823 |
+
biggie.loc[:20, "B"] = np.nan
|
| 824 |
+
s = biggie.to_string()
|
| 825 |
+
|
| 826 |
+
buf = StringIO()
|
| 827 |
+
retval = biggie.to_string(buf=buf)
|
| 828 |
+
assert retval is None
|
| 829 |
+
assert buf.getvalue() == s
|
| 830 |
+
|
| 831 |
+
assert isinstance(s, str)
|
| 832 |
+
|
| 833 |
+
# print in right order
|
| 834 |
+
result = biggie.to_string(
|
| 835 |
+
columns=["B", "A"], col_space=17, float_format="%.5f".__mod__
|
| 836 |
+
)
|
| 837 |
+
lines = result.split("\n")
|
| 838 |
+
header = lines[0].strip().split()
|
| 839 |
+
joined = "\n".join([re.sub(r"\s+", " ", x).strip() for x in lines[1:]])
|
| 840 |
+
recons = read_csv(StringIO(joined), names=header, header=None, sep=" ")
|
| 841 |
+
tm.assert_series_equal(recons["B"], biggie["B"])
|
| 842 |
+
assert recons["A"].count() == biggie["A"].count()
|
| 843 |
+
assert (np.abs(recons["A"].dropna() - biggie["A"].dropna()) < 0.1).all()
|
| 844 |
+
|
| 845 |
+
# FIXME: don't leave commented-out
|
| 846 |
+
# expected = ['B', 'A']
|
| 847 |
+
# assert header == expected
|
| 848 |
+
|
| 849 |
+
result = biggie.to_string(columns=["A"], col_space=17)
|
| 850 |
+
header = result.split("\n")[0].strip().split()
|
| 851 |
+
expected = ["A"]
|
| 852 |
+
assert header == expected
|
| 853 |
+
|
| 854 |
+
biggie.to_string(columns=["B", "A"], formatters={"A": lambda x: f"{x:.1f}"})
|
| 855 |
+
|
| 856 |
+
biggie.to_string(columns=["B", "A"], float_format=str)
|
| 857 |
+
biggie.to_string(columns=["B", "A"], col_space=12, float_format=str)
|
| 858 |
+
|
| 859 |
+
frame = DataFrame(index=np.arange(200))
|
| 860 |
+
frame.to_string()
|
| 861 |
+
|
| 862 |
+
# TODO: split or simplify this test?
|
| 863 |
+
@pytest.mark.xfail(using_string_dtype(), reason="fix when arrow is default")
|
| 864 |
+
def test_to_string_index_with_nan(self):
|
| 865 |
+
# GH#2850
|
| 866 |
+
df = DataFrame(
|
| 867 |
+
{
|
| 868 |
+
"id1": {0: "1a3", 1: "9h4"},
|
| 869 |
+
"id2": {0: np.nan, 1: "d67"},
|
| 870 |
+
"id3": {0: "78d", 1: "79d"},
|
| 871 |
+
"value": {0: 123, 1: 64},
|
| 872 |
+
}
|
| 873 |
+
)
|
| 874 |
+
|
| 875 |
+
# multi-index
|
| 876 |
+
y = df.set_index(["id1", "id2", "id3"])
|
| 877 |
+
result = y.to_string()
|
| 878 |
+
expected = (
|
| 879 |
+
" value\nid1 id2 id3 \n"
|
| 880 |
+
"1a3 NaN 78d 123\n9h4 d67 79d 64"
|
| 881 |
+
)
|
| 882 |
+
assert result == expected
|
| 883 |
+
|
| 884 |
+
# index
|
| 885 |
+
y = df.set_index("id2")
|
| 886 |
+
result = y.to_string()
|
| 887 |
+
expected = (
|
| 888 |
+
" id1 id3 value\nid2 \n"
|
| 889 |
+
"NaN 1a3 78d 123\nd67 9h4 79d 64"
|
| 890 |
+
)
|
| 891 |
+
assert result == expected
|
| 892 |
+
|
| 893 |
+
# with append (this failed in 0.12)
|
| 894 |
+
y = df.set_index(["id1", "id2"]).set_index("id3", append=True)
|
| 895 |
+
result = y.to_string()
|
| 896 |
+
expected = (
|
| 897 |
+
" value\nid1 id2 id3 \n"
|
| 898 |
+
"1a3 NaN 78d 123\n9h4 d67 79d 64"
|
| 899 |
+
)
|
| 900 |
+
assert result == expected
|
| 901 |
+
|
| 902 |
+
# all-nan in mi
|
| 903 |
+
df2 = df.copy()
|
| 904 |
+
df2.loc[:, "id2"] = np.nan
|
| 905 |
+
y = df2.set_index("id2")
|
| 906 |
+
result = y.to_string()
|
| 907 |
+
expected = (
|
| 908 |
+
" id1 id3 value\nid2 \n"
|
| 909 |
+
"NaN 1a3 78d 123\nNaN 9h4 79d 64"
|
| 910 |
+
)
|
| 911 |
+
assert result == expected
|
| 912 |
+
|
| 913 |
+
# partial nan in mi
|
| 914 |
+
df2 = df.copy()
|
| 915 |
+
df2.loc[:, "id2"] = np.nan
|
| 916 |
+
y = df2.set_index(["id2", "id3"])
|
| 917 |
+
result = y.to_string()
|
| 918 |
+
expected = (
|
| 919 |
+
" id1 value\nid2 id3 \n"
|
| 920 |
+
"NaN 78d 1a3 123\n 79d 9h4 64"
|
| 921 |
+
)
|
| 922 |
+
assert result == expected
|
| 923 |
+
|
| 924 |
+
df = DataFrame(
|
| 925 |
+
{
|
| 926 |
+
"id1": {0: np.nan, 1: "9h4"},
|
| 927 |
+
"id2": {0: np.nan, 1: "d67"},
|
| 928 |
+
"id3": {0: np.nan, 1: "79d"},
|
| 929 |
+
"value": {0: 123, 1: 64},
|
| 930 |
+
}
|
| 931 |
+
)
|
| 932 |
+
|
| 933 |
+
y = df.set_index(["id1", "id2", "id3"])
|
| 934 |
+
result = y.to_string()
|
| 935 |
+
expected = (
|
| 936 |
+
" value\nid1 id2 id3 \n"
|
| 937 |
+
"NaN NaN NaN 123\n9h4 d67 79d 64"
|
| 938 |
+
)
|
| 939 |
+
assert result == expected
|
| 940 |
+
|
| 941 |
+
def test_to_string_nonunicode_nonascii_alignment(self):
|
| 942 |
+
df = DataFrame([["aa\xc3\xa4\xc3\xa4", 1], ["bbbb", 2]])
|
| 943 |
+
rep_str = df.to_string()
|
| 944 |
+
lines = rep_str.split("\n")
|
| 945 |
+
assert len(lines[1]) == len(lines[2])
|
| 946 |
+
|
| 947 |
+
def test_unicode_problem_decoding_as_ascii(self):
|
| 948 |
+
df = DataFrame({"c/\u03c3": Series({"test": np.nan})})
|
| 949 |
+
str(df.to_string())
|
| 950 |
+
|
| 951 |
+
def test_to_string_repr_unicode(self):
|
| 952 |
+
buf = StringIO()
|
| 953 |
+
|
| 954 |
+
unicode_values = ["\u03c3"] * 10
|
| 955 |
+
unicode_values = np.array(unicode_values, dtype=object)
|
| 956 |
+
df = DataFrame({"unicode": unicode_values})
|
| 957 |
+
df.to_string(col_space=10, buf=buf)
|
| 958 |
+
|
| 959 |
+
# it works!
|
| 960 |
+
repr(df)
|
| 961 |
+
# it works even if sys.stdin in None
|
| 962 |
+
_stdin = sys.stdin
|
| 963 |
+
try:
|
| 964 |
+
sys.stdin = None
|
| 965 |
+
repr(df)
|
| 966 |
+
finally:
|
| 967 |
+
sys.stdin = _stdin
|
| 968 |
+
|
| 969 |
+
def test_nested_dataframe(self):
|
| 970 |
+
df1 = DataFrame({"level1": [["row1"], ["row2"]]})
|
| 971 |
+
df2 = DataFrame({"level3": [{"level2": df1}]})
|
| 972 |
+
result = df2.to_string()
|
| 973 |
+
expected = " level3\n0 {'level2': ['level1']}"
|
| 974 |
+
assert result == expected
|
| 975 |
+
|
| 976 |
+
|
| 977 |
+
class TestSeriesToString:
|
| 978 |
+
def test_to_string_without_index(self):
|
| 979 |
+
# GH#11729 Test index=False option
|
| 980 |
+
ser = Series([1, 2, 3, 4])
|
| 981 |
+
result = ser.to_string(index=False)
|
| 982 |
+
expected = "\n".join(["1", "2", "3", "4"])
|
| 983 |
+
assert result == expected
|
| 984 |
+
|
| 985 |
+
def test_to_string_name(self):
|
| 986 |
+
ser = Series(range(100), dtype="int64")
|
| 987 |
+
ser.name = "myser"
|
| 988 |
+
res = ser.to_string(max_rows=2, name=True)
|
| 989 |
+
exp = "0 0\n ..\n99 99\nName: myser"
|
| 990 |
+
assert res == exp
|
| 991 |
+
res = ser.to_string(max_rows=2, name=False)
|
| 992 |
+
exp = "0 0\n ..\n99 99"
|
| 993 |
+
assert res == exp
|
| 994 |
+
|
| 995 |
+
def test_to_string_dtype(self):
|
| 996 |
+
ser = Series(range(100), dtype="int64")
|
| 997 |
+
res = ser.to_string(max_rows=2, dtype=True)
|
| 998 |
+
exp = "0 0\n ..\n99 99\ndtype: int64"
|
| 999 |
+
assert res == exp
|
| 1000 |
+
res = ser.to_string(max_rows=2, dtype=False)
|
| 1001 |
+
exp = "0 0\n ..\n99 99"
|
| 1002 |
+
assert res == exp
|
| 1003 |
+
|
| 1004 |
+
def test_to_string_length(self):
|
| 1005 |
+
ser = Series(range(100), dtype="int64")
|
| 1006 |
+
res = ser.to_string(max_rows=2, length=True)
|
| 1007 |
+
exp = "0 0\n ..\n99 99\nLength: 100"
|
| 1008 |
+
assert res == exp
|
| 1009 |
+
|
| 1010 |
+
def test_to_string_na_rep(self):
|
| 1011 |
+
ser = Series(index=range(100), dtype=np.float64)
|
| 1012 |
+
res = ser.to_string(na_rep="foo", max_rows=2)
|
| 1013 |
+
exp = "0 foo\n ..\n99 foo"
|
| 1014 |
+
assert res == exp
|
| 1015 |
+
|
| 1016 |
+
def test_to_string_float_format(self):
|
| 1017 |
+
ser = Series(range(10), dtype="float64")
|
| 1018 |
+
res = ser.to_string(float_format=lambda x: f"{x:2.1f}", max_rows=2)
|
| 1019 |
+
exp = "0 0.0\n ..\n9 9.0"
|
| 1020 |
+
assert res == exp
|
| 1021 |
+
|
| 1022 |
+
def test_to_string_header(self):
|
| 1023 |
+
ser = Series(range(10), dtype="int64")
|
| 1024 |
+
ser.index.name = "foo"
|
| 1025 |
+
res = ser.to_string(header=True, max_rows=2)
|
| 1026 |
+
exp = "foo\n0 0\n ..\n9 9"
|
| 1027 |
+
assert res == exp
|
| 1028 |
+
res = ser.to_string(header=False, max_rows=2)
|
| 1029 |
+
exp = "0 0\n ..\n9 9"
|
| 1030 |
+
assert res == exp
|
| 1031 |
+
|
| 1032 |
+
def test_to_string_empty_col(self):
|
| 1033 |
+
# GH#13653
|
| 1034 |
+
ser = Series(["", "Hello", "World", "", "", "Mooooo", "", ""])
|
| 1035 |
+
res = ser.to_string(index=False)
|
| 1036 |
+
exp = " \n Hello\n World\n \n \nMooooo\n \n "
|
| 1037 |
+
assert re.match(exp, res)
|
| 1038 |
+
|
| 1039 |
+
def test_to_string_timedelta64(self):
|
| 1040 |
+
Series(np.array([1100, 20], dtype="timedelta64[ns]")).to_string()
|
| 1041 |
+
|
| 1042 |
+
ser = Series(date_range("2012-1-1", periods=3, freq="D"))
|
| 1043 |
+
|
| 1044 |
+
# GH#2146
|
| 1045 |
+
|
| 1046 |
+
# adding NaTs
|
| 1047 |
+
y = ser - ser.shift(1)
|
| 1048 |
+
result = y.to_string()
|
| 1049 |
+
assert "1 days" in result
|
| 1050 |
+
assert "00:00:00" not in result
|
| 1051 |
+
assert "NaT" in result
|
| 1052 |
+
|
| 1053 |
+
# with frac seconds
|
| 1054 |
+
o = Series([datetime(2012, 1, 1, microsecond=150)] * 3)
|
| 1055 |
+
y = ser - o
|
| 1056 |
+
result = y.to_string()
|
| 1057 |
+
assert "-1 days +23:59:59.999850" in result
|
| 1058 |
+
|
| 1059 |
+
# rounding?
|
| 1060 |
+
o = Series([datetime(2012, 1, 1, 1)] * 3)
|
| 1061 |
+
y = ser - o
|
| 1062 |
+
result = y.to_string()
|
| 1063 |
+
assert "-1 days +23:00:00" in result
|
| 1064 |
+
assert "1 days 23:00:00" in result
|
| 1065 |
+
|
| 1066 |
+
o = Series([datetime(2012, 1, 1, 1, 1)] * 3)
|
| 1067 |
+
y = ser - o
|
| 1068 |
+
result = y.to_string()
|
| 1069 |
+
assert "-1 days +22:59:00" in result
|
| 1070 |
+
assert "1 days 22:59:00" in result
|
| 1071 |
+
|
| 1072 |
+
o = Series([datetime(2012, 1, 1, 1, 1, microsecond=150)] * 3)
|
| 1073 |
+
y = ser - o
|
| 1074 |
+
result = y.to_string()
|
| 1075 |
+
assert "-1 days +22:58:59.999850" in result
|
| 1076 |
+
assert "0 days 22:58:59.999850" in result
|
| 1077 |
+
|
| 1078 |
+
# neg time
|
| 1079 |
+
td = timedelta(minutes=5, seconds=3)
|
| 1080 |
+
s2 = Series(date_range("2012-1-1", periods=3, freq="D")) + td
|
| 1081 |
+
y = ser - s2
|
| 1082 |
+
result = y.to_string()
|
| 1083 |
+
assert "-1 days +23:54:57" in result
|
| 1084 |
+
|
| 1085 |
+
td = timedelta(microseconds=550)
|
| 1086 |
+
s2 = Series(date_range("2012-1-1", periods=3, freq="D")) + td
|
| 1087 |
+
y = ser - td
|
| 1088 |
+
result = y.to_string()
|
| 1089 |
+
assert "2012-01-01 23:59:59.999450" in result
|
| 1090 |
+
|
| 1091 |
+
# no boxing of the actual elements
|
| 1092 |
+
td = Series(timedelta_range("1 days", periods=3))
|
| 1093 |
+
result = td.to_string()
|
| 1094 |
+
assert result == "0 1 days\n1 2 days\n2 3 days"
|
| 1095 |
+
|
| 1096 |
+
def test_to_string(self):
|
| 1097 |
+
ts = Series(
|
| 1098 |
+
np.arange(10, dtype=np.float64),
|
| 1099 |
+
index=date_range("2020-01-01", periods=10, freq="B"),
|
| 1100 |
+
)
|
| 1101 |
+
buf = StringIO()
|
| 1102 |
+
|
| 1103 |
+
s = ts.to_string()
|
| 1104 |
+
|
| 1105 |
+
retval = ts.to_string(buf=buf)
|
| 1106 |
+
assert retval is None
|
| 1107 |
+
assert buf.getvalue().strip() == s
|
| 1108 |
+
|
| 1109 |
+
# pass float_format
|
| 1110 |
+
format = "%.4f".__mod__
|
| 1111 |
+
result = ts.to_string(float_format=format)
|
| 1112 |
+
result = [x.split()[1] for x in result.split("\n")[:-1]]
|
| 1113 |
+
expected = [format(x) for x in ts]
|
| 1114 |
+
assert result == expected
|
| 1115 |
+
|
| 1116 |
+
# empty string
|
| 1117 |
+
result = ts[:0].to_string()
|
| 1118 |
+
assert result == "Series([], Freq: B)"
|
| 1119 |
+
|
| 1120 |
+
result = ts[:0].to_string(length=0)
|
| 1121 |
+
assert result == "Series([], Freq: B)"
|
| 1122 |
+
|
| 1123 |
+
# name and length
|
| 1124 |
+
cp = ts.copy()
|
| 1125 |
+
cp.name = "foo"
|
| 1126 |
+
result = cp.to_string(length=True, name=True, dtype=True)
|
| 1127 |
+
last_line = result.split("\n")[-1].strip()
|
| 1128 |
+
assert last_line == (f"Freq: B, Name: foo, Length: {len(cp)}, dtype: float64")
|
| 1129 |
+
|
| 1130 |
+
@pytest.mark.parametrize(
|
| 1131 |
+
"input_array, expected",
|
| 1132 |
+
[
|
| 1133 |
+
("a", "a"),
|
| 1134 |
+
(["a", "b"], "a\nb"),
|
| 1135 |
+
([1, "a"], "1\na"),
|
| 1136 |
+
(1, "1"),
|
| 1137 |
+
([0, -1], " 0\n-1"),
|
| 1138 |
+
(1.0, "1.0"),
|
| 1139 |
+
([" a", " b"], " a\n b"),
|
| 1140 |
+
([".1", "1"], ".1\n 1"),
|
| 1141 |
+
(["10", "-10"], " 10\n-10"),
|
| 1142 |
+
],
|
| 1143 |
+
)
|
| 1144 |
+
def test_format_remove_leading_space_series(self, input_array, expected):
|
| 1145 |
+
# GH: 24980
|
| 1146 |
+
ser = Series(input_array)
|
| 1147 |
+
result = ser.to_string(index=False)
|
| 1148 |
+
assert result == expected
|
| 1149 |
+
|
| 1150 |
+
def test_to_string_complex_number_trims_zeros(self):
|
| 1151 |
+
ser = Series([1.000000 + 1.000000j, 1.0 + 1.0j, 1.05 + 1.0j])
|
| 1152 |
+
result = ser.to_string()
|
| 1153 |
+
expected = dedent(
|
| 1154 |
+
"""\
|
| 1155 |
+
0 1.00+1.00j
|
| 1156 |
+
1 1.00+1.00j
|
| 1157 |
+
2 1.05+1.00j"""
|
| 1158 |
+
)
|
| 1159 |
+
assert result == expected
|
| 1160 |
+
|
| 1161 |
+
def test_nullable_float_to_string(self, float_ea_dtype):
|
| 1162 |
+
# https://github.com/pandas-dev/pandas/issues/36775
|
| 1163 |
+
dtype = float_ea_dtype
|
| 1164 |
+
ser = Series([0.0, 1.0, None], dtype=dtype)
|
| 1165 |
+
result = ser.to_string()
|
| 1166 |
+
expected = dedent(
|
| 1167 |
+
"""\
|
| 1168 |
+
0 0.0
|
| 1169 |
+
1 1.0
|
| 1170 |
+
2 <NA>"""
|
| 1171 |
+
)
|
| 1172 |
+
assert result == expected
|
| 1173 |
+
|
| 1174 |
+
def test_nullable_int_to_string(self, any_int_ea_dtype):
|
| 1175 |
+
# https://github.com/pandas-dev/pandas/issues/36775
|
| 1176 |
+
dtype = any_int_ea_dtype
|
| 1177 |
+
ser = Series([0, 1, None], dtype=dtype)
|
| 1178 |
+
result = ser.to_string()
|
| 1179 |
+
expected = dedent(
|
| 1180 |
+
"""\
|
| 1181 |
+
0 0
|
| 1182 |
+
1 1
|
| 1183 |
+
2 <NA>"""
|
| 1184 |
+
)
|
| 1185 |
+
assert result == expected
|
| 1186 |
+
|
| 1187 |
+
def test_to_string_mixed(self):
|
| 1188 |
+
ser = Series(["foo", np.nan, -1.23, 4.56])
|
| 1189 |
+
result = ser.to_string()
|
| 1190 |
+
expected = "".join(["0 foo\n", "1 NaN\n", "2 -1.23\n", "3 4.56"])
|
| 1191 |
+
assert result == expected
|
| 1192 |
+
|
| 1193 |
+
# but don't count NAs as floats
|
| 1194 |
+
ser = Series(["foo", np.nan, "bar", "baz"])
|
| 1195 |
+
result = ser.to_string()
|
| 1196 |
+
expected = "".join(["0 foo\n", "1 NaN\n", "2 bar\n", "3 baz"])
|
| 1197 |
+
assert result == expected
|
| 1198 |
+
|
| 1199 |
+
ser = Series(["foo", 5, "bar", "baz"])
|
| 1200 |
+
result = ser.to_string()
|
| 1201 |
+
expected = "".join(["0 foo\n", "1 5\n", "2 bar\n", "3 baz"])
|
| 1202 |
+
assert result == expected
|
| 1203 |
+
|
| 1204 |
+
def test_to_string_float_na_spacing(self):
|
| 1205 |
+
ser = Series([0.0, 1.5678, 2.0, -3.0, 4.0])
|
| 1206 |
+
ser[::2] = np.nan
|
| 1207 |
+
|
| 1208 |
+
result = ser.to_string()
|
| 1209 |
+
expected = "0 NaN\n1 1.5678\n2 NaN\n3 -3.0000\n4 NaN"
|
| 1210 |
+
assert result == expected
|
| 1211 |
+
|
| 1212 |
+
def test_to_string_with_datetimeindex(self):
|
| 1213 |
+
index = date_range("20130102", periods=6)
|
| 1214 |
+
ser = Series(1, index=index)
|
| 1215 |
+
result = ser.to_string()
|
| 1216 |
+
assert "2013-01-02" in result
|
| 1217 |
+
|
| 1218 |
+
# nat in index
|
| 1219 |
+
s2 = Series(2, index=[Timestamp("20130111"), NaT])
|
| 1220 |
+
ser = concat([s2, ser])
|
| 1221 |
+
result = ser.to_string()
|
| 1222 |
+
assert "NaT" in result
|
| 1223 |
+
|
| 1224 |
+
# nat in summary
|
| 1225 |
+
result = str(s2.index)
|
| 1226 |
+
assert "NaT" in result
|
.venv/lib/python3.13/site-packages/pandas/tests/io/generate_legacy_storage_files.py
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
self-contained to write legacy storage pickle files
|
| 3 |
+
|
| 4 |
+
To use this script. Create an environment where you want
|
| 5 |
+
generate pickles, say its for 0.20.3, with your pandas clone
|
| 6 |
+
in ~/pandas
|
| 7 |
+
|
| 8 |
+
. activate pandas_0.20.3
|
| 9 |
+
cd ~/pandas/pandas
|
| 10 |
+
|
| 11 |
+
$ python -m tests.io.generate_legacy_storage_files \
|
| 12 |
+
tests/io/data/legacy_pickle/0.20.3/ pickle
|
| 13 |
+
|
| 14 |
+
This script generates a storage file for the current arch, system,
|
| 15 |
+
and python version
|
| 16 |
+
pandas version: 0.20.3
|
| 17 |
+
output dir : pandas/pandas/tests/io/data/legacy_pickle/0.20.3/
|
| 18 |
+
storage format: pickle
|
| 19 |
+
created pickle file: 0.20.3_x86_64_darwin_3.5.2.pickle
|
| 20 |
+
|
| 21 |
+
The idea here is you are using the *current* version of the
|
| 22 |
+
generate_legacy_storage_files with an *older* version of pandas to
|
| 23 |
+
generate a pickle file. We will then check this file into a current
|
| 24 |
+
branch, and test using test_pickle.py. This will load the *older*
|
| 25 |
+
pickles and test versus the current data that is generated
|
| 26 |
+
(with main). These are then compared.
|
| 27 |
+
|
| 28 |
+
If we have cases where we changed the signature (e.g. we renamed
|
| 29 |
+
offset -> freq in Timestamp). Then we have to conditionally execute
|
| 30 |
+
in the generate_legacy_storage_files.py to make it
|
| 31 |
+
run under the older AND the newer version.
|
| 32 |
+
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
from datetime import timedelta
|
| 36 |
+
import os
|
| 37 |
+
import pickle
|
| 38 |
+
import platform as pl
|
| 39 |
+
import sys
|
| 40 |
+
|
| 41 |
+
# Remove script directory from path, otherwise Python will try to
|
| 42 |
+
# import the JSON test directory as the json module
|
| 43 |
+
sys.path.pop(0)
|
| 44 |
+
|
| 45 |
+
import numpy as np
|
| 46 |
+
|
| 47 |
+
import pandas
|
| 48 |
+
from pandas import (
|
| 49 |
+
Categorical,
|
| 50 |
+
DataFrame,
|
| 51 |
+
Index,
|
| 52 |
+
MultiIndex,
|
| 53 |
+
NaT,
|
| 54 |
+
Period,
|
| 55 |
+
RangeIndex,
|
| 56 |
+
Series,
|
| 57 |
+
Timestamp,
|
| 58 |
+
bdate_range,
|
| 59 |
+
date_range,
|
| 60 |
+
interval_range,
|
| 61 |
+
period_range,
|
| 62 |
+
timedelta_range,
|
| 63 |
+
)
|
| 64 |
+
from pandas.arrays import SparseArray
|
| 65 |
+
|
| 66 |
+
from pandas.tseries.offsets import (
|
| 67 |
+
FY5253,
|
| 68 |
+
BusinessDay,
|
| 69 |
+
BusinessHour,
|
| 70 |
+
CustomBusinessDay,
|
| 71 |
+
DateOffset,
|
| 72 |
+
Day,
|
| 73 |
+
Easter,
|
| 74 |
+
Hour,
|
| 75 |
+
LastWeekOfMonth,
|
| 76 |
+
Minute,
|
| 77 |
+
MonthBegin,
|
| 78 |
+
MonthEnd,
|
| 79 |
+
QuarterBegin,
|
| 80 |
+
QuarterEnd,
|
| 81 |
+
SemiMonthBegin,
|
| 82 |
+
SemiMonthEnd,
|
| 83 |
+
Week,
|
| 84 |
+
WeekOfMonth,
|
| 85 |
+
YearBegin,
|
| 86 |
+
YearEnd,
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _create_sp_series():
|
| 91 |
+
nan = np.nan
|
| 92 |
+
|
| 93 |
+
# nan-based
|
| 94 |
+
arr = np.arange(15, dtype=np.float64)
|
| 95 |
+
arr[7:12] = nan
|
| 96 |
+
arr[-1:] = nan
|
| 97 |
+
|
| 98 |
+
bseries = Series(SparseArray(arr, kind="block"))
|
| 99 |
+
bseries.name = "bseries"
|
| 100 |
+
return bseries
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def _create_sp_tsseries():
|
| 104 |
+
nan = np.nan
|
| 105 |
+
|
| 106 |
+
# nan-based
|
| 107 |
+
arr = np.arange(15, dtype=np.float64)
|
| 108 |
+
arr[7:12] = nan
|
| 109 |
+
arr[-1:] = nan
|
| 110 |
+
|
| 111 |
+
date_index = bdate_range("1/1/2011", periods=len(arr))
|
| 112 |
+
bseries = Series(SparseArray(arr, kind="block"), index=date_index)
|
| 113 |
+
bseries.name = "btsseries"
|
| 114 |
+
return bseries
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def _create_sp_frame():
|
| 118 |
+
nan = np.nan
|
| 119 |
+
|
| 120 |
+
data = {
|
| 121 |
+
"A": [nan, nan, nan, 0, 1, 2, 3, 4, 5, 6],
|
| 122 |
+
"B": [0, 1, 2, nan, nan, nan, 3, 4, 5, 6],
|
| 123 |
+
"C": np.arange(10).astype(np.int64),
|
| 124 |
+
"D": [0, 1, 2, 3, 4, 5, nan, nan, nan, nan],
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
dates = bdate_range("1/1/2011", periods=10)
|
| 128 |
+
return DataFrame(data, index=dates).apply(SparseArray)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def create_pickle_data(test: bool = True):
|
| 132 |
+
"""create the pickle data"""
|
| 133 |
+
data = {
|
| 134 |
+
"A": [0.0, 1.0, 2.0, 3.0, np.nan],
|
| 135 |
+
"B": [0, 1, 0, 1, 0],
|
| 136 |
+
"C": ["foo1", "foo2", "foo3", "foo4", "foo5"],
|
| 137 |
+
"D": date_range("1/1/2009", periods=5),
|
| 138 |
+
"E": [0.0, 1, Timestamp("20100101"), "foo", 2.0],
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
scalars = {"timestamp": Timestamp("20130101"), "period": Period("2012", "M")}
|
| 142 |
+
|
| 143 |
+
index = {
|
| 144 |
+
"int": Index(np.arange(10)),
|
| 145 |
+
"date": date_range("20130101", periods=10),
|
| 146 |
+
"period": period_range("2013-01-01", freq="M", periods=10),
|
| 147 |
+
"float": Index(np.arange(10, dtype=np.float64)),
|
| 148 |
+
"uint": Index(np.arange(10, dtype=np.uint64)),
|
| 149 |
+
"timedelta": timedelta_range("00:00:00", freq="30min", periods=10),
|
| 150 |
+
"string": Index(["foo", "bar", "baz", "qux", "quux"], dtype="string"),
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
index["range"] = RangeIndex(10)
|
| 154 |
+
|
| 155 |
+
index["interval"] = interval_range(0, periods=10)
|
| 156 |
+
|
| 157 |
+
mi = {
|
| 158 |
+
"reg2": MultiIndex.from_tuples(
|
| 159 |
+
tuple(
|
| 160 |
+
zip(
|
| 161 |
+
*[
|
| 162 |
+
["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"],
|
| 163 |
+
["one", "two", "one", "two", "one", "two", "one", "two"],
|
| 164 |
+
]
|
| 165 |
+
)
|
| 166 |
+
),
|
| 167 |
+
names=["first", "second"],
|
| 168 |
+
)
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
series = {
|
| 172 |
+
"float": Series(data["A"]),
|
| 173 |
+
"int": Series(data["B"]),
|
| 174 |
+
"mixed": Series(data["E"]),
|
| 175 |
+
"ts": Series(
|
| 176 |
+
np.arange(10).astype(np.int64), index=date_range("20130101", periods=10)
|
| 177 |
+
),
|
| 178 |
+
"mi": Series(
|
| 179 |
+
np.arange(5).astype(np.float64),
|
| 180 |
+
index=MultiIndex.from_tuples(
|
| 181 |
+
tuple(zip(*[[1, 1, 2, 2, 2], [3, 4, 3, 4, 5]])), names=["one", "two"]
|
| 182 |
+
),
|
| 183 |
+
),
|
| 184 |
+
"dup": Series(np.arange(5).astype(np.float64), index=["A", "B", "C", "D", "A"]),
|
| 185 |
+
"cat": Series(Categorical(["foo", "bar", "baz"])),
|
| 186 |
+
"dt": Series(date_range("20130101", periods=5)),
|
| 187 |
+
"dt_tz": Series(date_range("20130101", periods=5, tz="US/Eastern")),
|
| 188 |
+
"period": Series([Period("2000Q1")] * 5),
|
| 189 |
+
"string": Series(["foo", "bar", "baz", "qux", "quux"], dtype="string"),
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
mixed_dup_df = DataFrame(data)
|
| 193 |
+
mixed_dup_df.columns = list("ABCDA")
|
| 194 |
+
frame = {
|
| 195 |
+
"float": DataFrame({"A": series["float"], "B": series["float"] + 1}),
|
| 196 |
+
"int": DataFrame({"A": series["int"], "B": series["int"] + 1}),
|
| 197 |
+
"mixed": DataFrame({k: data[k] for k in ["A", "B", "C", "D"]}),
|
| 198 |
+
"mi": DataFrame(
|
| 199 |
+
{"A": np.arange(5).astype(np.float64), "B": np.arange(5).astype(np.int64)},
|
| 200 |
+
index=MultiIndex.from_tuples(
|
| 201 |
+
tuple(
|
| 202 |
+
zip(
|
| 203 |
+
*[
|
| 204 |
+
["bar", "bar", "baz", "baz", "baz"],
|
| 205 |
+
["one", "two", "one", "two", "three"],
|
| 206 |
+
]
|
| 207 |
+
)
|
| 208 |
+
),
|
| 209 |
+
names=["first", "second"],
|
| 210 |
+
),
|
| 211 |
+
),
|
| 212 |
+
"dup": DataFrame(
|
| 213 |
+
np.arange(15).reshape(5, 3).astype(np.float64), columns=["A", "B", "A"]
|
| 214 |
+
),
|
| 215 |
+
"cat_onecol": DataFrame({"A": Categorical(["foo", "bar"])}),
|
| 216 |
+
"cat_and_float": DataFrame(
|
| 217 |
+
{
|
| 218 |
+
"A": Categorical(["foo", "bar", "baz"]),
|
| 219 |
+
"B": np.arange(3).astype(np.int64),
|
| 220 |
+
}
|
| 221 |
+
),
|
| 222 |
+
"mixed_dup": mixed_dup_df,
|
| 223 |
+
"dt_mixed_tzs": DataFrame(
|
| 224 |
+
{
|
| 225 |
+
"A": Timestamp("20130102", tz="US/Eastern"),
|
| 226 |
+
"B": Timestamp("20130603", tz="CET"),
|
| 227 |
+
},
|
| 228 |
+
index=range(5),
|
| 229 |
+
),
|
| 230 |
+
"dt_mixed2_tzs": DataFrame(
|
| 231 |
+
{
|
| 232 |
+
"A": Timestamp("20130102", tz="US/Eastern"),
|
| 233 |
+
"B": Timestamp("20130603", tz="CET"),
|
| 234 |
+
"C": Timestamp("20130603", tz="UTC"),
|
| 235 |
+
},
|
| 236 |
+
index=range(5),
|
| 237 |
+
),
|
| 238 |
+
"string": DataFrame(
|
| 239 |
+
{
|
| 240 |
+
"A": Series(["foo", "bar", "baz", "qux", "quux"], dtype="string"),
|
| 241 |
+
"B": Series(["one", "two", "one", "two", "three"], dtype="string"),
|
| 242 |
+
}
|
| 243 |
+
),
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
cat = {
|
| 247 |
+
"int8": Categorical(list("abcdefg")),
|
| 248 |
+
"int16": Categorical(np.arange(1000)),
|
| 249 |
+
"int32": Categorical(np.arange(10000)),
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
timestamp = {
|
| 253 |
+
"normal": Timestamp("2011-01-01"),
|
| 254 |
+
"nat": NaT,
|
| 255 |
+
"tz": Timestamp("2011-01-01", tz="US/Eastern"),
|
| 256 |
+
}
|
| 257 |
+
if test:
|
| 258 |
+
# kept because those are present in the legacy pickles (<= 1.4)
|
| 259 |
+
timestamp["freq"] = Timestamp("2011-01-01")
|
| 260 |
+
timestamp["both"] = Timestamp("2011-01-01", tz="Asia/Tokyo")
|
| 261 |
+
|
| 262 |
+
off = {
|
| 263 |
+
"DateOffset": DateOffset(years=1),
|
| 264 |
+
"DateOffset_h_ns": DateOffset(hour=6, nanoseconds=5824),
|
| 265 |
+
"BusinessDay": BusinessDay(offset=timedelta(seconds=9)),
|
| 266 |
+
"BusinessHour": BusinessHour(normalize=True, n=6, end="15:14"),
|
| 267 |
+
"CustomBusinessDay": CustomBusinessDay(weekmask="Mon Fri"),
|
| 268 |
+
"SemiMonthBegin": SemiMonthBegin(day_of_month=9),
|
| 269 |
+
"SemiMonthEnd": SemiMonthEnd(day_of_month=24),
|
| 270 |
+
"MonthBegin": MonthBegin(1),
|
| 271 |
+
"MonthEnd": MonthEnd(1),
|
| 272 |
+
"QuarterBegin": QuarterBegin(1),
|
| 273 |
+
"QuarterEnd": QuarterEnd(1),
|
| 274 |
+
"Day": Day(1),
|
| 275 |
+
"YearBegin": YearBegin(1),
|
| 276 |
+
"YearEnd": YearEnd(1),
|
| 277 |
+
"Week": Week(1),
|
| 278 |
+
"Week_Tues": Week(2, normalize=False, weekday=1),
|
| 279 |
+
"WeekOfMonth": WeekOfMonth(week=3, weekday=4),
|
| 280 |
+
"LastWeekOfMonth": LastWeekOfMonth(n=1, weekday=3),
|
| 281 |
+
"FY5253": FY5253(n=2, weekday=6, startingMonth=7, variation="last"),
|
| 282 |
+
"Easter": Easter(),
|
| 283 |
+
"Hour": Hour(1),
|
| 284 |
+
"Minute": Minute(1),
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
return {
|
| 288 |
+
"series": series,
|
| 289 |
+
"frame": frame,
|
| 290 |
+
"index": index,
|
| 291 |
+
"scalars": scalars,
|
| 292 |
+
"mi": mi,
|
| 293 |
+
"sp_series": {"float": _create_sp_series(), "ts": _create_sp_tsseries()},
|
| 294 |
+
"sp_frame": {"float": _create_sp_frame()},
|
| 295 |
+
"cat": cat,
|
| 296 |
+
"timestamp": timestamp,
|
| 297 |
+
"offsets": off,
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
def create_dataframe_all_types():
|
| 302 |
+
timestamps = Series(
|
| 303 |
+
[
|
| 304 |
+
Timestamp("2013-01-01"),
|
| 305 |
+
NaT,
|
| 306 |
+
Timestamp("2013-01-03"),
|
| 307 |
+
Timestamp("2013-01-04"),
|
| 308 |
+
Timestamp("2013-01-05"),
|
| 309 |
+
]
|
| 310 |
+
)
|
| 311 |
+
timedeltas = timestamps - timestamps[0]
|
| 312 |
+
|
| 313 |
+
data = {
|
| 314 |
+
# "string": Series(
|
| 315 |
+
# ["a", "b", "c", None, "e"], dtype=StringDtype(na_value=np.nan)
|
| 316 |
+
# ),
|
| 317 |
+
# "object": Series(["a", "b", "c", None, "e"], dtype=object),
|
| 318 |
+
# "object_nan": Series(["a", "b", "c", np.nan, "e"], dtype=object),
|
| 319 |
+
"int": list(range(1, 6)),
|
| 320 |
+
"uint64": np.arange(3, 8).astype("uint64"),
|
| 321 |
+
"float": [0.1, 0.2, 0.3, 0.4, np.nan],
|
| 322 |
+
"float32": Series([0.1, 0.2, 0.3, 0.4, np.nan], dtype="float32"),
|
| 323 |
+
"bool": [True, False, True, False, True],
|
| 324 |
+
"datetime_ns": timestamps.dt.as_unit("ns"),
|
| 325 |
+
"datetime_us": timestamps.dt.as_unit("us"),
|
| 326 |
+
"datetime_ms": timestamps.dt.as_unit("ms"),
|
| 327 |
+
"datetime_s": timestamps.dt.as_unit("s"),
|
| 328 |
+
"datetimetz_ns": timestamps.dt.tz_localize("US/Eastern").dt.as_unit("ns"),
|
| 329 |
+
"datetimetz_us": timestamps.dt.tz_localize("US/Eastern").dt.as_unit("us"),
|
| 330 |
+
"timedelta_ns": timedeltas.dt.as_unit("ns"),
|
| 331 |
+
"timedelta_us": timedeltas.dt.as_unit("us"),
|
| 332 |
+
"timedelta_ms": timedeltas.dt.as_unit("ms"),
|
| 333 |
+
"timedelta_s": timedeltas.dt.as_unit("s"),
|
| 334 |
+
# "categorical": Categorical(
|
| 335 |
+
# Series(
|
| 336 |
+
# ["foo", "bar", "baz",np.nan,"foo"],dtype=StringDtype(na_value=np.nan)
|
| 337 |
+
# )
|
| 338 |
+
# ),
|
| 339 |
+
# "categorical_object": Categorical(
|
| 340 |
+
# Series(["foo", "bar", "baz", np.nan, "foo"], dtype=object)
|
| 341 |
+
# ),
|
| 342 |
+
"categorical_int": Categorical([1, 2, 3, np.nan, 1]),
|
| 343 |
+
}
|
| 344 |
+
return DataFrame(data)
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
def platform_name():
|
| 348 |
+
return "_".join(
|
| 349 |
+
[
|
| 350 |
+
str(pandas.__version__),
|
| 351 |
+
str(pl.machine()),
|
| 352 |
+
str(pl.system().lower()),
|
| 353 |
+
str(pl.python_version()),
|
| 354 |
+
]
|
| 355 |
+
)
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
def write_legacy_pickles(output_dir):
|
| 359 |
+
pth = f"{platform_name()}.pickle"
|
| 360 |
+
|
| 361 |
+
with open(os.path.join(output_dir, pth), "wb") as fh:
|
| 362 |
+
pickle.dump(create_pickle_data(test=False), fh, pickle.DEFAULT_PROTOCOL)
|
| 363 |
+
|
| 364 |
+
print(f"created pickle file: {pth}")
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
def write_legacy_hdf(output_dir, format):
|
| 368 |
+
import tables
|
| 369 |
+
|
| 370 |
+
pth = f"{platform_name()}_pytables-{tables.__version__}_{format}.h5"
|
| 371 |
+
|
| 372 |
+
df = create_dataframe_all_types()
|
| 373 |
+
if format == "fixed":
|
| 374 |
+
# df = df.drop(columns=["categorical", "categorical_object", "categorical_int"])
|
| 375 |
+
df = df.drop(columns=["categorical_int"])
|
| 376 |
+
complevel = 9 if format == "table" else None
|
| 377 |
+
df.to_hdf(
|
| 378 |
+
os.path.join(output_dir, pth),
|
| 379 |
+
key="df_alltypes",
|
| 380 |
+
format=format,
|
| 381 |
+
complevel=complevel,
|
| 382 |
+
)
|
| 383 |
+
|
| 384 |
+
print(f"created hdf file: {pth}")
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
def write_legacy_file():
|
| 388 |
+
# force our cwd to be the first searched
|
| 389 |
+
sys.path.insert(0, "")
|
| 390 |
+
|
| 391 |
+
if not 3 <= len(sys.argv) <= 4:
|
| 392 |
+
sys.exit(
|
| 393 |
+
"Specify output directory and storage type: generate_legacy_"
|
| 394 |
+
"storage_files.py <output_dir> <storage_type> "
|
| 395 |
+
)
|
| 396 |
+
|
| 397 |
+
output_dir = str(sys.argv[1])
|
| 398 |
+
storage_type = str(sys.argv[2])
|
| 399 |
+
|
| 400 |
+
print(
|
| 401 |
+
"This script generates a storage file for the current arch, system, "
|
| 402 |
+
"and python version"
|
| 403 |
+
)
|
| 404 |
+
print(f" pandas version: {pandas.__version__}")
|
| 405 |
+
print(f" output dir : {output_dir}")
|
| 406 |
+
print(f" storage format: {storage_type}")
|
| 407 |
+
|
| 408 |
+
if not os.path.exists(output_dir):
|
| 409 |
+
os.mkdir(output_dir)
|
| 410 |
+
|
| 411 |
+
if storage_type == "pickle":
|
| 412 |
+
write_legacy_pickles(output_dir=output_dir)
|
| 413 |
+
elif storage_type == "hdf":
|
| 414 |
+
write_legacy_hdf(output_dir=output_dir, format="fixed")
|
| 415 |
+
write_legacy_hdf(output_dir=output_dir, format="table")
|
| 416 |
+
else:
|
| 417 |
+
sys.exit("storage_type must be one of {'pickle', 'hdf'}")
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
if __name__ == "__main__":
|
| 421 |
+
write_legacy_file()
|
.venv/lib/python3.13/site-packages/pandas/tests/io/json/__init__.py
ADDED
|
File without changes
|
.venv/lib/python3.13/site-packages/pandas/tests/io/json/conftest.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
@pytest.fixture(params=["split", "records", "index", "columns", "values"])
|
| 5 |
+
def orient(request):
|
| 6 |
+
"""
|
| 7 |
+
Fixture for orients excluding the table format.
|
| 8 |
+
"""
|
| 9 |
+
return request.param
|
.venv/lib/python3.13/site-packages/pandas/tests/io/json/test_compression.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from io import (
|
| 2 |
+
BytesIO,
|
| 3 |
+
StringIO,
|
| 4 |
+
)
|
| 5 |
+
import uuid
|
| 6 |
+
|
| 7 |
+
import pytest
|
| 8 |
+
|
| 9 |
+
import pandas.util._test_decorators as td
|
| 10 |
+
|
| 11 |
+
import pandas as pd
|
| 12 |
+
import pandas._testing as tm
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def test_compression_roundtrip(compression, temp_file):
|
| 16 |
+
df = pd.DataFrame(
|
| 17 |
+
[[0.123456, 0.234567, 0.567567], [12.32112, 123123.2, 321321.2]],
|
| 18 |
+
index=["A", "B"],
|
| 19 |
+
columns=["X", "Y", "Z"],
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
df.to_json(temp_file, compression=compression)
|
| 23 |
+
tm.assert_frame_equal(df, pd.read_json(temp_file, compression=compression))
|
| 24 |
+
|
| 25 |
+
# explicitly ensure file was compressed.
|
| 26 |
+
with tm.decompress_file(temp_file, compression) as fh:
|
| 27 |
+
result = fh.read().decode("utf8")
|
| 28 |
+
data = StringIO(result)
|
| 29 |
+
tm.assert_frame_equal(df, pd.read_json(data))
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def test_read_zipped_json(datapath):
|
| 33 |
+
uncompressed_path = datapath("io", "json", "data", "tsframe_v012.json")
|
| 34 |
+
uncompressed_df = pd.read_json(uncompressed_path)
|
| 35 |
+
|
| 36 |
+
compressed_path = datapath("io", "json", "data", "tsframe_v012.json.zip")
|
| 37 |
+
compressed_df = pd.read_json(compressed_path, compression="zip")
|
| 38 |
+
|
| 39 |
+
tm.assert_frame_equal(uncompressed_df, compressed_df)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@td.skip_if_not_us_locale
|
| 43 |
+
@pytest.mark.single_cpu
|
| 44 |
+
@pytest.mark.network
|
| 45 |
+
def test_with_s3_url(compression, s3_bucket_public, s3so, temp_file):
|
| 46 |
+
# Bucket created in tests/io/conftest.py
|
| 47 |
+
df = pd.read_json(StringIO('{"a": [1, 2, 3], "b": [4, 5, 6]}'))
|
| 48 |
+
|
| 49 |
+
key = f"{uuid.uuid4()}.json"
|
| 50 |
+
df.to_json(temp_file, compression=compression)
|
| 51 |
+
with open(temp_file, "rb") as f:
|
| 52 |
+
s3_bucket_public.put_object(Key=key, Body=f)
|
| 53 |
+
|
| 54 |
+
roundtripped_df = pd.read_json(
|
| 55 |
+
f"s3://{s3_bucket_public.name}/{key}",
|
| 56 |
+
compression=compression,
|
| 57 |
+
storage_options=s3so,
|
| 58 |
+
)
|
| 59 |
+
tm.assert_frame_equal(df, roundtripped_df)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def test_lines_with_compression(compression, temp_file):
|
| 63 |
+
df = pd.read_json(StringIO('{"a": [1, 2, 3], "b": [4, 5, 6]}'))
|
| 64 |
+
df.to_json(temp_file, orient="records", lines=True, compression=compression)
|
| 65 |
+
roundtripped_df = pd.read_json(temp_file, lines=True, compression=compression)
|
| 66 |
+
tm.assert_frame_equal(df, roundtripped_df)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def test_chunksize_with_compression(compression, temp_file):
|
| 70 |
+
df = pd.read_json(StringIO('{"a": ["foo", "bar", "baz"], "b": [4, 5, 6]}'))
|
| 71 |
+
df.to_json(temp_file, orient="records", lines=True, compression=compression)
|
| 72 |
+
|
| 73 |
+
with pd.read_json(
|
| 74 |
+
temp_file, lines=True, chunksize=1, compression=compression
|
| 75 |
+
) as res:
|
| 76 |
+
roundtripped_df = pd.concat(res)
|
| 77 |
+
tm.assert_frame_equal(df, roundtripped_df)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def test_write_unsupported_compression_type(temp_file):
|
| 81 |
+
df = pd.read_json(StringIO('{"a": [1, 2, 3], "b": [4, 5, 6]}'))
|
| 82 |
+
msg = "Unrecognized compression type: unsupported"
|
| 83 |
+
with pytest.raises(ValueError, match=msg):
|
| 84 |
+
df.to_json(temp_file, compression="unsupported")
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def test_read_unsupported_compression_type(temp_file):
|
| 88 |
+
msg = "Unrecognized compression type: unsupported"
|
| 89 |
+
with pytest.raises(ValueError, match=msg):
|
| 90 |
+
pd.read_json(temp_file, compression="unsupported")
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
@pytest.mark.parametrize(
|
| 94 |
+
"infer_string", [False, pytest.param(True, marks=td.skip_if_no("pyarrow"))]
|
| 95 |
+
)
|
| 96 |
+
@pytest.mark.parametrize("to_infer", [True, False])
|
| 97 |
+
@pytest.mark.parametrize("read_infer", [True, False])
|
| 98 |
+
def test_to_json_compression(
|
| 99 |
+
compression_only,
|
| 100 |
+
read_infer,
|
| 101 |
+
to_infer,
|
| 102 |
+
compression_to_extension,
|
| 103 |
+
infer_string,
|
| 104 |
+
tmp_path,
|
| 105 |
+
):
|
| 106 |
+
with pd.option_context("future.infer_string", infer_string):
|
| 107 |
+
# see gh-15008
|
| 108 |
+
compression = compression_only
|
| 109 |
+
|
| 110 |
+
# We'll complete file extension subsequently.
|
| 111 |
+
filename = tmp_path / f"test.{compression_to_extension[compression]}"
|
| 112 |
+
|
| 113 |
+
df = pd.DataFrame({"A": [1]})
|
| 114 |
+
|
| 115 |
+
to_compression = "infer" if to_infer else compression
|
| 116 |
+
read_compression = "infer" if read_infer else compression
|
| 117 |
+
|
| 118 |
+
df.to_json(filename, compression=to_compression)
|
| 119 |
+
result = pd.read_json(filename, compression=read_compression)
|
| 120 |
+
tm.assert_frame_equal(result, df)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def test_to_json_compression_mode(compression):
|
| 124 |
+
# GH 39985 (read_json does not support user-provided binary files)
|
| 125 |
+
expected = pd.DataFrame({"A": [1]})
|
| 126 |
+
|
| 127 |
+
with BytesIO() as buffer:
|
| 128 |
+
expected.to_json(buffer, compression=compression)
|
| 129 |
+
# df = pd.read_json(buffer, compression=compression)
|
| 130 |
+
# tm.assert_frame_equal(expected, df)
|
.venv/lib/python3.13/site-packages/pandas/tests/io/json/test_deprecated_kwargs.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tests for the deprecated keyword arguments for `read_json`.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from io import StringIO
|
| 6 |
+
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import pandas._testing as tm
|
| 9 |
+
|
| 10 |
+
from pandas.io.json import read_json
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def test_good_kwargs():
|
| 14 |
+
df = pd.DataFrame({"A": [2, 4, 6], "B": [3, 6, 9]}, index=[0, 1, 2])
|
| 15 |
+
|
| 16 |
+
with tm.assert_produces_warning(None):
|
| 17 |
+
data1 = StringIO(df.to_json(orient="split"))
|
| 18 |
+
tm.assert_frame_equal(df, read_json(data1, orient="split"))
|
| 19 |
+
data2 = StringIO(df.to_json(orient="columns"))
|
| 20 |
+
tm.assert_frame_equal(df, read_json(data2, orient="columns"))
|
| 21 |
+
data3 = StringIO(df.to_json(orient="index"))
|
| 22 |
+
tm.assert_frame_equal(df, read_json(data3, orient="index"))
|
.venv/lib/python3.13/site-packages/pandas/tests/io/json/test_json_table_schema.py
ADDED
|
@@ -0,0 +1,898 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for Table Schema integration."""
|
| 2 |
+
|
| 3 |
+
from collections import OrderedDict
|
| 4 |
+
from io import StringIO
|
| 5 |
+
import json
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import pytest
|
| 9 |
+
|
| 10 |
+
from pandas.errors import Pandas4Warning
|
| 11 |
+
|
| 12 |
+
from pandas.core.dtypes.dtypes import (
|
| 13 |
+
CategoricalDtype,
|
| 14 |
+
DatetimeTZDtype,
|
| 15 |
+
PeriodDtype,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
import pandas as pd
|
| 19 |
+
from pandas import DataFrame
|
| 20 |
+
import pandas._testing as tm
|
| 21 |
+
|
| 22 |
+
from pandas.io.json._table_schema import (
|
| 23 |
+
as_json_table_type,
|
| 24 |
+
build_table_schema,
|
| 25 |
+
convert_json_field_to_pandas_type,
|
| 26 |
+
convert_pandas_type_to_json_field,
|
| 27 |
+
set_default_names,
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@pytest.fixture
|
| 32 |
+
def df_schema():
|
| 33 |
+
return DataFrame(
|
| 34 |
+
{
|
| 35 |
+
"A": [1, 2, 3, 4],
|
| 36 |
+
"B": ["a", "b", "c", "c"],
|
| 37 |
+
"C": pd.date_range("2016-01-01", freq="D", periods=4),
|
| 38 |
+
"D": pd.timedelta_range("1h", periods=4, freq="min"),
|
| 39 |
+
},
|
| 40 |
+
index=pd.Index(range(4), name="idx"),
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@pytest.fixture
|
| 45 |
+
def df_table():
|
| 46 |
+
return DataFrame(
|
| 47 |
+
{
|
| 48 |
+
"A": [1, 2, 3, 4],
|
| 49 |
+
"B": ["a", "b", "c", "c"],
|
| 50 |
+
"C": pd.date_range("2016-01-01", freq="D", periods=4),
|
| 51 |
+
"D": pd.timedelta_range("1h", periods=4, freq="min"),
|
| 52 |
+
"E": pd.Series(pd.Categorical(["a", "b", "c", "c"])),
|
| 53 |
+
"F": pd.Series(pd.Categorical(["a", "b", "c", "c"], ordered=True)),
|
| 54 |
+
"G": [1.0, 2.0, 3, 4.0],
|
| 55 |
+
"H": pd.date_range("2016-01-01", freq="D", periods=4, tz="US/Central"),
|
| 56 |
+
},
|
| 57 |
+
index=pd.Index(range(4), name="idx"),
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class TestBuildSchema:
|
| 62 |
+
def test_build_table_schema(self, df_schema, using_infer_string):
|
| 63 |
+
result = build_table_schema(df_schema, version=False)
|
| 64 |
+
expected = {
|
| 65 |
+
"fields": [
|
| 66 |
+
{"name": "idx", "type": "integer"},
|
| 67 |
+
{"name": "A", "type": "integer"},
|
| 68 |
+
{"name": "B", "type": "string"},
|
| 69 |
+
{"name": "C", "type": "datetime"},
|
| 70 |
+
{"name": "D", "type": "duration"},
|
| 71 |
+
],
|
| 72 |
+
"primaryKey": ["idx"],
|
| 73 |
+
}
|
| 74 |
+
if using_infer_string:
|
| 75 |
+
expected["fields"][2] = {"name": "B", "type": "string", "extDtype": "str"}
|
| 76 |
+
assert result == expected
|
| 77 |
+
result = build_table_schema(df_schema)
|
| 78 |
+
assert "pandas_version" in result
|
| 79 |
+
|
| 80 |
+
def test_series(self):
|
| 81 |
+
s = pd.Series([1, 2, 3], name="foo")
|
| 82 |
+
result = build_table_schema(s, version=False)
|
| 83 |
+
expected = {
|
| 84 |
+
"fields": [
|
| 85 |
+
{"name": "index", "type": "integer"},
|
| 86 |
+
{"name": "foo", "type": "integer"},
|
| 87 |
+
],
|
| 88 |
+
"primaryKey": ["index"],
|
| 89 |
+
}
|
| 90 |
+
assert result == expected
|
| 91 |
+
result = build_table_schema(s)
|
| 92 |
+
assert "pandas_version" in result
|
| 93 |
+
|
| 94 |
+
def test_series_unnamed(self):
|
| 95 |
+
result = build_table_schema(pd.Series([1, 2, 3]), version=False)
|
| 96 |
+
expected = {
|
| 97 |
+
"fields": [
|
| 98 |
+
{"name": "index", "type": "integer"},
|
| 99 |
+
{"name": "values", "type": "integer"},
|
| 100 |
+
],
|
| 101 |
+
"primaryKey": ["index"],
|
| 102 |
+
}
|
| 103 |
+
assert result == expected
|
| 104 |
+
|
| 105 |
+
def test_multiindex(self, df_schema, using_infer_string):
|
| 106 |
+
df = df_schema
|
| 107 |
+
idx = pd.MultiIndex.from_product([("a", "b"), (1, 2)])
|
| 108 |
+
df.index = idx
|
| 109 |
+
|
| 110 |
+
result = build_table_schema(df, version=False)
|
| 111 |
+
expected = {
|
| 112 |
+
"fields": [
|
| 113 |
+
{"name": "level_0", "type": "string"},
|
| 114 |
+
{"name": "level_1", "type": "integer"},
|
| 115 |
+
{"name": "A", "type": "integer"},
|
| 116 |
+
{"name": "B", "type": "string"},
|
| 117 |
+
{"name": "C", "type": "datetime"},
|
| 118 |
+
{"name": "D", "type": "duration"},
|
| 119 |
+
],
|
| 120 |
+
"primaryKey": ["level_0", "level_1"],
|
| 121 |
+
}
|
| 122 |
+
if using_infer_string:
|
| 123 |
+
expected["fields"][0] = {
|
| 124 |
+
"name": "level_0",
|
| 125 |
+
"type": "string",
|
| 126 |
+
"extDtype": "str",
|
| 127 |
+
}
|
| 128 |
+
expected["fields"][3] = {"name": "B", "type": "string", "extDtype": "str"}
|
| 129 |
+
assert result == expected
|
| 130 |
+
|
| 131 |
+
df.index.names = ["idx0", None]
|
| 132 |
+
expected["fields"][0]["name"] = "idx0"
|
| 133 |
+
expected["primaryKey"] = ["idx0", "level_1"]
|
| 134 |
+
result = build_table_schema(df, version=False)
|
| 135 |
+
assert result == expected
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
class TestTableSchemaType:
|
| 139 |
+
@pytest.mark.parametrize("int_type", [int, np.int16, np.int32, np.int64])
|
| 140 |
+
def test_as_json_table_type_int_data(self, int_type):
|
| 141 |
+
int_data = [1, 2, 3]
|
| 142 |
+
assert as_json_table_type(np.array(int_data, dtype=int_type).dtype) == "integer"
|
| 143 |
+
|
| 144 |
+
@pytest.mark.parametrize("float_type", [float, np.float16, np.float32, np.float64])
|
| 145 |
+
def test_as_json_table_type_float_data(self, float_type):
|
| 146 |
+
float_data = [1.0, 2.0, 3.0]
|
| 147 |
+
assert (
|
| 148 |
+
as_json_table_type(np.array(float_data, dtype=float_type).dtype) == "number"
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
@pytest.mark.parametrize("bool_type", [bool, np.bool_])
|
| 152 |
+
def test_as_json_table_type_bool_data(self, bool_type):
|
| 153 |
+
bool_data = [True, False]
|
| 154 |
+
assert (
|
| 155 |
+
as_json_table_type(np.array(bool_data, dtype=bool_type).dtype) == "boolean"
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
@pytest.mark.parametrize(
|
| 159 |
+
"date_data",
|
| 160 |
+
[
|
| 161 |
+
pd.to_datetime(["2016"]),
|
| 162 |
+
pd.to_datetime(["2016"], utc=True),
|
| 163 |
+
pd.Series(pd.to_datetime(["2016"])),
|
| 164 |
+
pd.Series(pd.to_datetime(["2016"], utc=True)),
|
| 165 |
+
pd.period_range("2016", freq="Y", periods=3),
|
| 166 |
+
],
|
| 167 |
+
)
|
| 168 |
+
def test_as_json_table_type_date_data(self, date_data):
|
| 169 |
+
assert as_json_table_type(date_data.dtype) == "datetime"
|
| 170 |
+
|
| 171 |
+
@pytest.mark.parametrize("klass", [pd.Series, pd.Index])
|
| 172 |
+
def test_as_json_table_type_string_data(self, klass):
|
| 173 |
+
str_data = klass(["a", "b"], dtype=object)
|
| 174 |
+
assert as_json_table_type(str_data.dtype) == "string"
|
| 175 |
+
|
| 176 |
+
@pytest.mark.parametrize(
|
| 177 |
+
"cat_data",
|
| 178 |
+
[
|
| 179 |
+
pd.Categorical(["a"]),
|
| 180 |
+
pd.Categorical([1]),
|
| 181 |
+
pd.Series(pd.Categorical([1])),
|
| 182 |
+
pd.CategoricalIndex([1]),
|
| 183 |
+
],
|
| 184 |
+
)
|
| 185 |
+
def test_as_json_table_type_categorical_data(self, cat_data):
|
| 186 |
+
assert as_json_table_type(cat_data.dtype) == "any"
|
| 187 |
+
|
| 188 |
+
# ------
|
| 189 |
+
# dtypes
|
| 190 |
+
# ------
|
| 191 |
+
@pytest.mark.parametrize("int_dtype", [int, np.int16, np.int32, np.int64])
|
| 192 |
+
def test_as_json_table_type_int_dtypes(self, int_dtype):
|
| 193 |
+
assert as_json_table_type(int_dtype) == "integer"
|
| 194 |
+
|
| 195 |
+
@pytest.mark.parametrize("float_dtype", [float, np.float16, np.float32, np.float64])
|
| 196 |
+
def test_as_json_table_type_float_dtypes(self, float_dtype):
|
| 197 |
+
assert as_json_table_type(float_dtype) == "number"
|
| 198 |
+
|
| 199 |
+
@pytest.mark.parametrize("bool_dtype", [bool, np.bool_])
|
| 200 |
+
def test_as_json_table_type_bool_dtypes(self, bool_dtype):
|
| 201 |
+
assert as_json_table_type(bool_dtype) == "boolean"
|
| 202 |
+
|
| 203 |
+
@pytest.mark.parametrize(
|
| 204 |
+
"date_dtype",
|
| 205 |
+
[
|
| 206 |
+
np.dtype("<M8[ns]"),
|
| 207 |
+
PeriodDtype("D"),
|
| 208 |
+
DatetimeTZDtype("ns", "US/Central"),
|
| 209 |
+
],
|
| 210 |
+
)
|
| 211 |
+
def test_as_json_table_type_date_dtypes(self, date_dtype):
|
| 212 |
+
# TODO: datedate.date? datetime.time?
|
| 213 |
+
assert as_json_table_type(date_dtype) == "datetime"
|
| 214 |
+
|
| 215 |
+
@pytest.mark.parametrize("td_dtype", [np.dtype("<m8[ns]")])
|
| 216 |
+
def test_as_json_table_type_timedelta_dtypes(self, td_dtype):
|
| 217 |
+
assert as_json_table_type(td_dtype) == "duration"
|
| 218 |
+
|
| 219 |
+
@pytest.mark.parametrize("str_dtype", [object]) # TODO(GH#14904) flesh out dtypes?
|
| 220 |
+
def test_as_json_table_type_string_dtypes(self, str_dtype):
|
| 221 |
+
assert as_json_table_type(str_dtype) == "string"
|
| 222 |
+
|
| 223 |
+
def test_as_json_table_type_categorical_dtypes(self):
|
| 224 |
+
assert as_json_table_type(pd.Categorical(["a"]).dtype) == "any"
|
| 225 |
+
assert as_json_table_type(CategoricalDtype()) == "any"
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
class TestTableOrient:
|
| 229 |
+
def test_build_series(self):
|
| 230 |
+
s = pd.Series([1, 2], name="a")
|
| 231 |
+
s.index.name = "id"
|
| 232 |
+
result = s.to_json(orient="table", date_format="iso")
|
| 233 |
+
result = json.loads(result, object_pairs_hook=OrderedDict)
|
| 234 |
+
|
| 235 |
+
assert "pandas_version" in result["schema"]
|
| 236 |
+
result["schema"].pop("pandas_version")
|
| 237 |
+
|
| 238 |
+
fields = [{"name": "id", "type": "integer"}, {"name": "a", "type": "integer"}]
|
| 239 |
+
|
| 240 |
+
schema = {"fields": fields, "primaryKey": ["id"]}
|
| 241 |
+
|
| 242 |
+
expected = OrderedDict(
|
| 243 |
+
[
|
| 244 |
+
("schema", schema),
|
| 245 |
+
(
|
| 246 |
+
"data",
|
| 247 |
+
[
|
| 248 |
+
OrderedDict([("id", 0), ("a", 1)]),
|
| 249 |
+
OrderedDict([("id", 1), ("a", 2)]),
|
| 250 |
+
],
|
| 251 |
+
),
|
| 252 |
+
]
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
assert result == expected
|
| 256 |
+
|
| 257 |
+
def test_read_json_from_to_json_results(self):
|
| 258 |
+
# GH32383
|
| 259 |
+
df = DataFrame(
|
| 260 |
+
{
|
| 261 |
+
"_id": {"row_0": 0},
|
| 262 |
+
"category": {"row_0": "Goods"},
|
| 263 |
+
"recommender_id": {"row_0": 3},
|
| 264 |
+
"recommender_name_jp": {"row_0": "浦田"},
|
| 265 |
+
"recommender_name_en": {"row_0": "Urata"},
|
| 266 |
+
"name_jp": {"row_0": "博多人形(松尾吉将まつお よしまさ)"},
|
| 267 |
+
"name_en": {"row_0": "Hakata Dolls Matsuo"},
|
| 268 |
+
}
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
result1 = pd.read_json(StringIO(df.to_json()))
|
| 272 |
+
result2 = DataFrame.from_dict(json.loads(df.to_json()))
|
| 273 |
+
tm.assert_frame_equal(result1, df)
|
| 274 |
+
tm.assert_frame_equal(result2, df)
|
| 275 |
+
|
| 276 |
+
def test_to_json(self, df_table, using_infer_string):
|
| 277 |
+
df = df_table
|
| 278 |
+
df.index.name = "idx"
|
| 279 |
+
result = df.to_json(orient="table", date_format="iso")
|
| 280 |
+
result = json.loads(result, object_pairs_hook=OrderedDict)
|
| 281 |
+
|
| 282 |
+
assert "pandas_version" in result["schema"]
|
| 283 |
+
result["schema"].pop("pandas_version")
|
| 284 |
+
|
| 285 |
+
fields = [
|
| 286 |
+
{"name": "idx", "type": "integer"},
|
| 287 |
+
{"name": "A", "type": "integer"},
|
| 288 |
+
{"name": "B", "type": "string"},
|
| 289 |
+
{"name": "C", "type": "datetime"},
|
| 290 |
+
{"name": "D", "type": "duration"},
|
| 291 |
+
{
|
| 292 |
+
"constraints": {"enum": ["a", "b", "c"]},
|
| 293 |
+
"name": "E",
|
| 294 |
+
"ordered": False,
|
| 295 |
+
"type": "any",
|
| 296 |
+
},
|
| 297 |
+
{
|
| 298 |
+
"constraints": {"enum": ["a", "b", "c"]},
|
| 299 |
+
"name": "F",
|
| 300 |
+
"ordered": True,
|
| 301 |
+
"type": "any",
|
| 302 |
+
},
|
| 303 |
+
{"name": "G", "type": "number"},
|
| 304 |
+
{"name": "H", "type": "datetime", "tz": "US/Central"},
|
| 305 |
+
]
|
| 306 |
+
|
| 307 |
+
if using_infer_string:
|
| 308 |
+
fields[2] = {"name": "B", "type": "string", "extDtype": "str"}
|
| 309 |
+
|
| 310 |
+
schema = {"fields": fields, "primaryKey": ["idx"]}
|
| 311 |
+
data = [
|
| 312 |
+
OrderedDict(
|
| 313 |
+
[
|
| 314 |
+
("idx", 0),
|
| 315 |
+
("A", 1),
|
| 316 |
+
("B", "a"),
|
| 317 |
+
("C", "2016-01-01T00:00:00.000"),
|
| 318 |
+
("D", "P0DT1H0M0S"),
|
| 319 |
+
("E", "a"),
|
| 320 |
+
("F", "a"),
|
| 321 |
+
("G", 1.0),
|
| 322 |
+
("H", "2016-01-01T06:00:00.000Z"),
|
| 323 |
+
]
|
| 324 |
+
),
|
| 325 |
+
OrderedDict(
|
| 326 |
+
[
|
| 327 |
+
("idx", 1),
|
| 328 |
+
("A", 2),
|
| 329 |
+
("B", "b"),
|
| 330 |
+
("C", "2016-01-02T00:00:00.000"),
|
| 331 |
+
("D", "P0DT1H1M0S"),
|
| 332 |
+
("E", "b"),
|
| 333 |
+
("F", "b"),
|
| 334 |
+
("G", 2.0),
|
| 335 |
+
("H", "2016-01-02T06:00:00.000Z"),
|
| 336 |
+
]
|
| 337 |
+
),
|
| 338 |
+
OrderedDict(
|
| 339 |
+
[
|
| 340 |
+
("idx", 2),
|
| 341 |
+
("A", 3),
|
| 342 |
+
("B", "c"),
|
| 343 |
+
("C", "2016-01-03T00:00:00.000"),
|
| 344 |
+
("D", "P0DT1H2M0S"),
|
| 345 |
+
("E", "c"),
|
| 346 |
+
("F", "c"),
|
| 347 |
+
("G", 3.0),
|
| 348 |
+
("H", "2016-01-03T06:00:00.000Z"),
|
| 349 |
+
]
|
| 350 |
+
),
|
| 351 |
+
OrderedDict(
|
| 352 |
+
[
|
| 353 |
+
("idx", 3),
|
| 354 |
+
("A", 4),
|
| 355 |
+
("B", "c"),
|
| 356 |
+
("C", "2016-01-04T00:00:00.000"),
|
| 357 |
+
("D", "P0DT1H3M0S"),
|
| 358 |
+
("E", "c"),
|
| 359 |
+
("F", "c"),
|
| 360 |
+
("G", 4.0),
|
| 361 |
+
("H", "2016-01-04T06:00:00.000Z"),
|
| 362 |
+
]
|
| 363 |
+
),
|
| 364 |
+
]
|
| 365 |
+
expected = OrderedDict([("schema", schema), ("data", data)])
|
| 366 |
+
|
| 367 |
+
assert result == expected
|
| 368 |
+
|
| 369 |
+
def test_to_json_float_index(self):
|
| 370 |
+
data = pd.Series(1, index=[1.0, 2.0])
|
| 371 |
+
result = data.to_json(orient="table", date_format="iso")
|
| 372 |
+
result = json.loads(result, object_pairs_hook=OrderedDict)
|
| 373 |
+
result["schema"].pop("pandas_version")
|
| 374 |
+
|
| 375 |
+
expected = OrderedDict(
|
| 376 |
+
[
|
| 377 |
+
(
|
| 378 |
+
"schema",
|
| 379 |
+
{
|
| 380 |
+
"fields": [
|
| 381 |
+
{"name": "index", "type": "number"},
|
| 382 |
+
{"name": "values", "type": "integer"},
|
| 383 |
+
],
|
| 384 |
+
"primaryKey": ["index"],
|
| 385 |
+
},
|
| 386 |
+
),
|
| 387 |
+
(
|
| 388 |
+
"data",
|
| 389 |
+
[
|
| 390 |
+
OrderedDict([("index", 1.0), ("values", 1)]),
|
| 391 |
+
OrderedDict([("index", 2.0), ("values", 1)]),
|
| 392 |
+
],
|
| 393 |
+
),
|
| 394 |
+
]
|
| 395 |
+
)
|
| 396 |
+
|
| 397 |
+
assert result == expected
|
| 398 |
+
|
| 399 |
+
def test_to_json_period_index(self):
|
| 400 |
+
idx = pd.period_range("2016", freq="Q-JAN", periods=2)
|
| 401 |
+
data = pd.Series(1, idx)
|
| 402 |
+
result = data.to_json(orient="table", date_format="iso")
|
| 403 |
+
result = json.loads(result, object_pairs_hook=OrderedDict)
|
| 404 |
+
result["schema"].pop("pandas_version")
|
| 405 |
+
|
| 406 |
+
fields = [
|
| 407 |
+
{"freq": "QE-JAN", "name": "index", "type": "datetime"},
|
| 408 |
+
{"name": "values", "type": "integer"},
|
| 409 |
+
]
|
| 410 |
+
|
| 411 |
+
schema = {"fields": fields, "primaryKey": ["index"]}
|
| 412 |
+
data = [
|
| 413 |
+
OrderedDict([("index", "2015-11-01T00:00:00.000"), ("values", 1)]),
|
| 414 |
+
OrderedDict([("index", "2016-02-01T00:00:00.000"), ("values", 1)]),
|
| 415 |
+
]
|
| 416 |
+
expected = OrderedDict([("schema", schema), ("data", data)])
|
| 417 |
+
|
| 418 |
+
assert result == expected
|
| 419 |
+
|
| 420 |
+
def test_to_json_categorical_index(self):
|
| 421 |
+
data = pd.Series(1, pd.CategoricalIndex(["a", "b"]))
|
| 422 |
+
result = data.to_json(orient="table", date_format="iso")
|
| 423 |
+
result = json.loads(result, object_pairs_hook=OrderedDict)
|
| 424 |
+
result["schema"].pop("pandas_version")
|
| 425 |
+
|
| 426 |
+
expected = OrderedDict(
|
| 427 |
+
[
|
| 428 |
+
(
|
| 429 |
+
"schema",
|
| 430 |
+
{
|
| 431 |
+
"fields": [
|
| 432 |
+
{
|
| 433 |
+
"name": "index",
|
| 434 |
+
"type": "any",
|
| 435 |
+
"constraints": {"enum": ["a", "b"]},
|
| 436 |
+
"ordered": False,
|
| 437 |
+
},
|
| 438 |
+
{"name": "values", "type": "integer"},
|
| 439 |
+
],
|
| 440 |
+
"primaryKey": ["index"],
|
| 441 |
+
},
|
| 442 |
+
),
|
| 443 |
+
(
|
| 444 |
+
"data",
|
| 445 |
+
[
|
| 446 |
+
OrderedDict([("index", "a"), ("values", 1)]),
|
| 447 |
+
OrderedDict([("index", "b"), ("values", 1)]),
|
| 448 |
+
],
|
| 449 |
+
),
|
| 450 |
+
]
|
| 451 |
+
)
|
| 452 |
+
|
| 453 |
+
assert result == expected
|
| 454 |
+
|
| 455 |
+
def test_date_format_raises(self, df_table):
|
| 456 |
+
error_msg = (
|
| 457 |
+
"Trying to write with `orient='table'` and `date_format='epoch'`. Table "
|
| 458 |
+
"Schema requires dates to be formatted with `date_format='iso'`"
|
| 459 |
+
)
|
| 460 |
+
warning_msg = (
|
| 461 |
+
"'epoch' date format is deprecated and will be removed in a future "
|
| 462 |
+
"version, please use 'iso' date format instead."
|
| 463 |
+
)
|
| 464 |
+
with pytest.raises(ValueError, match=error_msg):
|
| 465 |
+
with tm.assert_produces_warning(Pandas4Warning, match=warning_msg):
|
| 466 |
+
df_table.to_json(orient="table", date_format="epoch")
|
| 467 |
+
|
| 468 |
+
# others work
|
| 469 |
+
df_table.to_json(orient="table", date_format="iso")
|
| 470 |
+
df_table.to_json(orient="table")
|
| 471 |
+
|
| 472 |
+
def test_convert_pandas_type_to_json_field_int(self, index_or_series):
|
| 473 |
+
kind = index_or_series
|
| 474 |
+
data = [1, 2, 3]
|
| 475 |
+
result = convert_pandas_type_to_json_field(kind(data, name="name"))
|
| 476 |
+
expected = {"name": "name", "type": "integer"}
|
| 477 |
+
assert result == expected
|
| 478 |
+
|
| 479 |
+
def test_convert_pandas_type_to_json_field_float(self, index_or_series):
|
| 480 |
+
kind = index_or_series
|
| 481 |
+
data = [1.0, 2.0, 3.0]
|
| 482 |
+
result = convert_pandas_type_to_json_field(kind(data, name="name"))
|
| 483 |
+
expected = {"name": "name", "type": "number"}
|
| 484 |
+
assert result == expected
|
| 485 |
+
|
| 486 |
+
@pytest.mark.parametrize(
|
| 487 |
+
"dt_args,extra_exp", [({}, {}), ({"utc": True}, {"tz": "UTC"})]
|
| 488 |
+
)
|
| 489 |
+
@pytest.mark.parametrize("wrapper", [None, pd.Series])
|
| 490 |
+
def test_convert_pandas_type_to_json_field_datetime(
|
| 491 |
+
self, dt_args, extra_exp, wrapper
|
| 492 |
+
):
|
| 493 |
+
data = [1.0, 2.0, 3.0]
|
| 494 |
+
data = pd.to_datetime(data, **dt_args)
|
| 495 |
+
if wrapper is pd.Series:
|
| 496 |
+
data = pd.Series(data, name="values")
|
| 497 |
+
result = convert_pandas_type_to_json_field(data)
|
| 498 |
+
expected = {"name": "values", "type": "datetime"}
|
| 499 |
+
expected.update(extra_exp)
|
| 500 |
+
assert result == expected
|
| 501 |
+
|
| 502 |
+
def test_convert_pandas_type_to_json_period_range(self):
|
| 503 |
+
arr = pd.period_range("2016", freq="Y-DEC", periods=4)
|
| 504 |
+
result = convert_pandas_type_to_json_field(arr)
|
| 505 |
+
expected = {"name": "values", "type": "datetime", "freq": "YE-DEC"}
|
| 506 |
+
assert result == expected
|
| 507 |
+
|
| 508 |
+
@pytest.mark.parametrize("kind", [pd.Categorical, pd.CategoricalIndex])
|
| 509 |
+
@pytest.mark.parametrize("ordered", [True, False])
|
| 510 |
+
def test_convert_pandas_type_to_json_field_categorical(self, kind, ordered):
|
| 511 |
+
data = ["a", "b", "c"]
|
| 512 |
+
if kind is pd.Categorical:
|
| 513 |
+
arr = pd.Series(kind(data, ordered=ordered), name="cats")
|
| 514 |
+
elif kind is pd.CategoricalIndex:
|
| 515 |
+
arr = kind(data, ordered=ordered, name="cats")
|
| 516 |
+
|
| 517 |
+
result = convert_pandas_type_to_json_field(arr)
|
| 518 |
+
expected = {
|
| 519 |
+
"name": "cats",
|
| 520 |
+
"type": "any",
|
| 521 |
+
"constraints": {"enum": data},
|
| 522 |
+
"ordered": ordered,
|
| 523 |
+
}
|
| 524 |
+
assert result == expected
|
| 525 |
+
|
| 526 |
+
@pytest.mark.parametrize(
|
| 527 |
+
"inp,exp",
|
| 528 |
+
[
|
| 529 |
+
({"type": "integer"}, "int64"),
|
| 530 |
+
({"type": "number"}, "float64"),
|
| 531 |
+
({"type": "boolean"}, "bool"),
|
| 532 |
+
({"type": "duration"}, "timedelta64"),
|
| 533 |
+
({"type": "datetime"}, "datetime64[ns]"),
|
| 534 |
+
({"type": "datetime", "tz": "US/Hawaii"}, "datetime64[ns, US/Hawaii]"),
|
| 535 |
+
({"type": "any"}, "object"),
|
| 536 |
+
(
|
| 537 |
+
{
|
| 538 |
+
"type": "any",
|
| 539 |
+
"constraints": {"enum": ["a", "b", "c"]},
|
| 540 |
+
"ordered": False,
|
| 541 |
+
},
|
| 542 |
+
CategoricalDtype(categories=["a", "b", "c"], ordered=False),
|
| 543 |
+
),
|
| 544 |
+
(
|
| 545 |
+
{
|
| 546 |
+
"type": "any",
|
| 547 |
+
"constraints": {"enum": ["a", "b", "c"]},
|
| 548 |
+
"ordered": True,
|
| 549 |
+
},
|
| 550 |
+
CategoricalDtype(categories=["a", "b", "c"], ordered=True),
|
| 551 |
+
),
|
| 552 |
+
({"type": "string"}, None),
|
| 553 |
+
],
|
| 554 |
+
)
|
| 555 |
+
def test_convert_json_field_to_pandas_type(self, inp, exp):
|
| 556 |
+
field = {"name": "foo"}
|
| 557 |
+
field.update(inp)
|
| 558 |
+
assert convert_json_field_to_pandas_type(field) == exp
|
| 559 |
+
|
| 560 |
+
@pytest.mark.parametrize("inp", ["geopoint", "geojson", "fake_type"])
|
| 561 |
+
def test_convert_json_field_to_pandas_type_raises(self, inp):
|
| 562 |
+
field = {"type": inp}
|
| 563 |
+
with pytest.raises(
|
| 564 |
+
ValueError, match=f"Unsupported or invalid field type: {inp}"
|
| 565 |
+
):
|
| 566 |
+
convert_json_field_to_pandas_type(field)
|
| 567 |
+
|
| 568 |
+
def test_categorical(self):
|
| 569 |
+
s = pd.Series(pd.Categorical(["a", "b", "a"]))
|
| 570 |
+
s.index.name = "idx"
|
| 571 |
+
result = s.to_json(orient="table", date_format="iso")
|
| 572 |
+
result = json.loads(result, object_pairs_hook=OrderedDict)
|
| 573 |
+
result["schema"].pop("pandas_version")
|
| 574 |
+
|
| 575 |
+
fields = [
|
| 576 |
+
{"name": "idx", "type": "integer"},
|
| 577 |
+
{
|
| 578 |
+
"constraints": {"enum": ["a", "b"]},
|
| 579 |
+
"name": "values",
|
| 580 |
+
"ordered": False,
|
| 581 |
+
"type": "any",
|
| 582 |
+
},
|
| 583 |
+
]
|
| 584 |
+
|
| 585 |
+
expected = OrderedDict(
|
| 586 |
+
[
|
| 587 |
+
("schema", {"fields": fields, "primaryKey": ["idx"]}),
|
| 588 |
+
(
|
| 589 |
+
"data",
|
| 590 |
+
[
|
| 591 |
+
OrderedDict([("idx", 0), ("values", "a")]),
|
| 592 |
+
OrderedDict([("idx", 1), ("values", "b")]),
|
| 593 |
+
OrderedDict([("idx", 2), ("values", "a")]),
|
| 594 |
+
],
|
| 595 |
+
),
|
| 596 |
+
]
|
| 597 |
+
)
|
| 598 |
+
|
| 599 |
+
assert result == expected
|
| 600 |
+
|
| 601 |
+
@pytest.mark.parametrize(
|
| 602 |
+
"idx,nm,prop",
|
| 603 |
+
[
|
| 604 |
+
(pd.Index([1]), "index", "name"),
|
| 605 |
+
(pd.Index([1], name="myname"), "myname", "name"),
|
| 606 |
+
(
|
| 607 |
+
pd.MultiIndex.from_product([("a", "b"), ("c", "d")]),
|
| 608 |
+
["level_0", "level_1"],
|
| 609 |
+
"names",
|
| 610 |
+
),
|
| 611 |
+
(
|
| 612 |
+
pd.MultiIndex.from_product(
|
| 613 |
+
[("a", "b"), ("c", "d")], names=["n1", "n2"]
|
| 614 |
+
),
|
| 615 |
+
["n1", "n2"],
|
| 616 |
+
"names",
|
| 617 |
+
),
|
| 618 |
+
(
|
| 619 |
+
pd.MultiIndex.from_product(
|
| 620 |
+
[("a", "b"), ("c", "d")], names=["n1", None]
|
| 621 |
+
),
|
| 622 |
+
["n1", "level_1"],
|
| 623 |
+
"names",
|
| 624 |
+
),
|
| 625 |
+
],
|
| 626 |
+
)
|
| 627 |
+
def test_set_names_unset(self, idx, nm, prop):
|
| 628 |
+
data = pd.Series(1, idx)
|
| 629 |
+
result = set_default_names(data)
|
| 630 |
+
assert getattr(result.index, prop) == nm
|
| 631 |
+
|
| 632 |
+
@pytest.mark.parametrize(
|
| 633 |
+
"idx",
|
| 634 |
+
[
|
| 635 |
+
pd.Index([], name="index"),
|
| 636 |
+
pd.MultiIndex.from_arrays([["foo"], ["bar"]], names=("level_0", "level_1")),
|
| 637 |
+
pd.MultiIndex.from_arrays([["foo"], ["bar"]], names=("foo", "level_1")),
|
| 638 |
+
],
|
| 639 |
+
)
|
| 640 |
+
def test_warns_non_roundtrippable_names(self, idx):
|
| 641 |
+
# GH 19130
|
| 642 |
+
df = DataFrame(index=idx)
|
| 643 |
+
df.index.name = "index"
|
| 644 |
+
with tm.assert_produces_warning(UserWarning, match="not round-trippable"):
|
| 645 |
+
set_default_names(df)
|
| 646 |
+
|
| 647 |
+
def test_timestamp_in_columns(self):
|
| 648 |
+
df = DataFrame(
|
| 649 |
+
[[1, 2]], columns=[pd.Timestamp("2016"), pd.Timedelta(10, unit="s")]
|
| 650 |
+
)
|
| 651 |
+
result = df.to_json(orient="table")
|
| 652 |
+
js = json.loads(result)
|
| 653 |
+
assert js["schema"]["fields"][1]["name"] == "2016-01-01T00:00:00.000"
|
| 654 |
+
assert js["schema"]["fields"][2]["name"] == "P0DT0H0M10S"
|
| 655 |
+
|
| 656 |
+
@pytest.mark.parametrize(
|
| 657 |
+
"case",
|
| 658 |
+
[
|
| 659 |
+
pd.Series([1], index=pd.Index([1], name="a"), name="a"),
|
| 660 |
+
DataFrame({"A": [1]}, index=pd.Index([1], name="A")),
|
| 661 |
+
DataFrame(
|
| 662 |
+
{"A": [1]},
|
| 663 |
+
index=pd.MultiIndex.from_arrays([["a"], [1]], names=["A", "a"]),
|
| 664 |
+
),
|
| 665 |
+
],
|
| 666 |
+
)
|
| 667 |
+
def test_overlapping_names(self, case):
|
| 668 |
+
with pytest.raises(ValueError, match="Overlapping"):
|
| 669 |
+
case.to_json(orient="table")
|
| 670 |
+
|
| 671 |
+
def test_mi_falsey_name(self):
|
| 672 |
+
# GH 16203
|
| 673 |
+
df = DataFrame(
|
| 674 |
+
np.random.default_rng(2).standard_normal((4, 4)),
|
| 675 |
+
index=pd.MultiIndex.from_product([("A", "B"), ("a", "b")]),
|
| 676 |
+
)
|
| 677 |
+
result = [x["name"] for x in build_table_schema(df)["fields"]]
|
| 678 |
+
assert result == ["level_0", "level_1", 0, 1, 2, 3]
|
| 679 |
+
|
| 680 |
+
|
| 681 |
+
class TestTableOrientReader:
|
| 682 |
+
@pytest.mark.parametrize(
|
| 683 |
+
"index_nm",
|
| 684 |
+
[None, "idx", pytest.param("index", marks=pytest.mark.xfail), "level_0"],
|
| 685 |
+
)
|
| 686 |
+
@pytest.mark.parametrize(
|
| 687 |
+
"vals",
|
| 688 |
+
[
|
| 689 |
+
{"ints": [1, 2, 3, 4]},
|
| 690 |
+
{"objects": ["a", "b", "c", "d"]},
|
| 691 |
+
{"objects": ["1", "2", "3", "4"]},
|
| 692 |
+
{
|
| 693 |
+
"date_ranges": pd.date_range(
|
| 694 |
+
"2016-01-01", freq="D", periods=4, unit="ns"
|
| 695 |
+
)
|
| 696 |
+
},
|
| 697 |
+
{"categoricals": pd.Series(pd.Categorical(["a", "b", "c", "c"]))},
|
| 698 |
+
{
|
| 699 |
+
"ordered_cats": pd.Series(
|
| 700 |
+
pd.Categorical(["a", "b", "c", "c"], ordered=True)
|
| 701 |
+
)
|
| 702 |
+
},
|
| 703 |
+
{"floats": [1.0, 2.0, 3.0, 4.0]},
|
| 704 |
+
{"floats": [1.1, 2.2, 3.3, 4.4]},
|
| 705 |
+
{"bools": [True, False, False, True]},
|
| 706 |
+
{
|
| 707 |
+
"timezones": pd.date_range(
|
| 708 |
+
"2016-01-01", freq="D", periods=4, tz="US/Central", unit="ns"
|
| 709 |
+
) # added in # GH 35973
|
| 710 |
+
},
|
| 711 |
+
],
|
| 712 |
+
)
|
| 713 |
+
def test_read_json_table_orient(self, index_nm, vals):
|
| 714 |
+
df = DataFrame(vals, index=pd.Index(range(4), name=index_nm))
|
| 715 |
+
out = StringIO(df.to_json(orient="table"))
|
| 716 |
+
result = pd.read_json(out, orient="table")
|
| 717 |
+
tm.assert_frame_equal(df, result)
|
| 718 |
+
|
| 719 |
+
@pytest.mark.parametrize(
|
| 720 |
+
"index_nm",
|
| 721 |
+
[
|
| 722 |
+
None,
|
| 723 |
+
"idx",
|
| 724 |
+
pytest.param(
|
| 725 |
+
"index",
|
| 726 |
+
marks=pytest.mark.filterwarnings("ignore:Index name:UserWarning"),
|
| 727 |
+
),
|
| 728 |
+
],
|
| 729 |
+
)
|
| 730 |
+
def test_read_json_table_orient_raises(self, index_nm):
|
| 731 |
+
vals = {"timedeltas": pd.timedelta_range("1h", periods=4, freq="min")}
|
| 732 |
+
df = DataFrame(vals, index=pd.Index(range(4), name=index_nm))
|
| 733 |
+
out = StringIO(df.to_json(orient="table"))
|
| 734 |
+
with pytest.raises(NotImplementedError, match="can not yet read "):
|
| 735 |
+
pd.read_json(out, orient="table")
|
| 736 |
+
|
| 737 |
+
@pytest.mark.parametrize(
|
| 738 |
+
"index_nm",
|
| 739 |
+
[None, "idx", pytest.param("index", marks=pytest.mark.xfail), "level_0"],
|
| 740 |
+
)
|
| 741 |
+
@pytest.mark.parametrize(
|
| 742 |
+
"vals",
|
| 743 |
+
[
|
| 744 |
+
{"ints": [1, 2, 3, 4]},
|
| 745 |
+
{"objects": ["a", "b", "c", "d"]},
|
| 746 |
+
{"objects": ["1", "2", "3", "4"]},
|
| 747 |
+
{
|
| 748 |
+
"date_ranges": pd.date_range(
|
| 749 |
+
"2016-01-01", freq="D", periods=4, unit="ns"
|
| 750 |
+
)
|
| 751 |
+
},
|
| 752 |
+
{"categoricals": pd.Series(pd.Categorical(["a", "b", "c", "c"]))},
|
| 753 |
+
{
|
| 754 |
+
"ordered_cats": pd.Series(
|
| 755 |
+
pd.Categorical(["a", "b", "c", "c"], ordered=True)
|
| 756 |
+
)
|
| 757 |
+
},
|
| 758 |
+
{"floats": [1.0, 2.0, 3.0, 4.0]},
|
| 759 |
+
{"floats": [1.1, 2.2, 3.3, 4.4]},
|
| 760 |
+
{"bools": [True, False, False, True]},
|
| 761 |
+
{
|
| 762 |
+
"timezones": pd.date_range(
|
| 763 |
+
"2016-01-01", freq="D", periods=4, tz="US/Central", unit="ns"
|
| 764 |
+
) # added in # GH 35973
|
| 765 |
+
},
|
| 766 |
+
],
|
| 767 |
+
)
|
| 768 |
+
def test_read_json_table_period_orient(self, index_nm, vals):
|
| 769 |
+
df = DataFrame(
|
| 770 |
+
vals,
|
| 771 |
+
index=pd.Index(
|
| 772 |
+
(pd.Period(f"2022Q{q}") for q in range(1, 5)), name=index_nm
|
| 773 |
+
),
|
| 774 |
+
)
|
| 775 |
+
out = StringIO(df.to_json(orient="table"))
|
| 776 |
+
result = pd.read_json(out, orient="table")
|
| 777 |
+
tm.assert_frame_equal(df, result)
|
| 778 |
+
|
| 779 |
+
@pytest.mark.parametrize(
|
| 780 |
+
"idx",
|
| 781 |
+
[
|
| 782 |
+
pd.Index(range(4)),
|
| 783 |
+
pd.date_range(
|
| 784 |
+
"2020-08-30",
|
| 785 |
+
freq="D",
|
| 786 |
+
periods=4,
|
| 787 |
+
unit="ns",
|
| 788 |
+
)._with_freq(None),
|
| 789 |
+
pd.date_range(
|
| 790 |
+
"2020-08-30", freq="D", periods=4, tz="US/Central", unit="ns"
|
| 791 |
+
)._with_freq(None),
|
| 792 |
+
pd.MultiIndex.from_product(
|
| 793 |
+
[
|
| 794 |
+
pd.date_range(
|
| 795 |
+
"2020-08-30", freq="D", periods=2, tz="US/Central", unit="ns"
|
| 796 |
+
),
|
| 797 |
+
["x", "y"],
|
| 798 |
+
],
|
| 799 |
+
),
|
| 800 |
+
],
|
| 801 |
+
)
|
| 802 |
+
@pytest.mark.parametrize(
|
| 803 |
+
"vals",
|
| 804 |
+
[
|
| 805 |
+
{"floats": [1.1, 2.2, 3.3, 4.4]},
|
| 806 |
+
{"dates": pd.date_range("2020-08-30", freq="D", periods=4, unit="ns")},
|
| 807 |
+
{
|
| 808 |
+
"timezones": pd.date_range(
|
| 809 |
+
"2020-08-30", freq="D", periods=4, tz="Europe/London", unit="ns"
|
| 810 |
+
)
|
| 811 |
+
},
|
| 812 |
+
],
|
| 813 |
+
)
|
| 814 |
+
def test_read_json_table_timezones_orient(self, idx, vals):
|
| 815 |
+
# GH 35973
|
| 816 |
+
df = DataFrame(vals, index=idx)
|
| 817 |
+
out = StringIO(df.to_json(orient="table"))
|
| 818 |
+
result = pd.read_json(out, orient="table")
|
| 819 |
+
tm.assert_frame_equal(df, result)
|
| 820 |
+
|
| 821 |
+
def test_comprehensive(self):
|
| 822 |
+
df = DataFrame(
|
| 823 |
+
{
|
| 824 |
+
"A": [1, 2, 3, 4],
|
| 825 |
+
"B": ["a", "b", "c", "c"],
|
| 826 |
+
"C": pd.date_range("2016-01-01", freq="D", periods=4, unit="ns"),
|
| 827 |
+
# 'D': pd.timedelta_range('1h', periods=4, freq='min'),
|
| 828 |
+
"E": pd.Series(pd.Categorical(["a", "b", "c", "c"])),
|
| 829 |
+
"F": pd.Series(pd.Categorical(["a", "b", "c", "c"], ordered=True)),
|
| 830 |
+
"G": [1.1, 2.2, 3.3, 4.4],
|
| 831 |
+
"H": pd.date_range(
|
| 832 |
+
"2016-01-01", freq="D", periods=4, tz="US/Central", unit="ns"
|
| 833 |
+
),
|
| 834 |
+
"I": [True, False, False, True],
|
| 835 |
+
},
|
| 836 |
+
index=pd.Index(range(4), name="idx"),
|
| 837 |
+
)
|
| 838 |
+
|
| 839 |
+
out = StringIO(df.to_json(orient="table"))
|
| 840 |
+
result = pd.read_json(out, orient="table")
|
| 841 |
+
tm.assert_frame_equal(df, result)
|
| 842 |
+
|
| 843 |
+
@pytest.mark.parametrize(
|
| 844 |
+
"index_names",
|
| 845 |
+
[[None, None], ["foo", "bar"], ["foo", None], [None, "foo"], ["index", "foo"]],
|
| 846 |
+
)
|
| 847 |
+
def test_multiindex(self, index_names):
|
| 848 |
+
# GH 18912
|
| 849 |
+
df = DataFrame(
|
| 850 |
+
[["Arr", "alpha", [1, 2, 3, 4]], ["Bee", "Beta", [10, 20, 30, 40]]],
|
| 851 |
+
index=[["A", "B"], ["Null", "Eins"]],
|
| 852 |
+
columns=["Aussprache", "Griechisch", "Args"],
|
| 853 |
+
)
|
| 854 |
+
df.index.names = index_names
|
| 855 |
+
out = StringIO(df.to_json(orient="table"))
|
| 856 |
+
result = pd.read_json(out, orient="table")
|
| 857 |
+
tm.assert_frame_equal(df, result)
|
| 858 |
+
|
| 859 |
+
def test_empty_frame_roundtrip(self):
|
| 860 |
+
# GH 21287
|
| 861 |
+
df = DataFrame(columns=["a", "b", "c"])
|
| 862 |
+
expected = df.copy()
|
| 863 |
+
out = StringIO(df.to_json(orient="table"))
|
| 864 |
+
result = pd.read_json(out, orient="table")
|
| 865 |
+
tm.assert_frame_equal(expected, result)
|
| 866 |
+
|
| 867 |
+
def test_read_json_orient_table_old_schema_version(self):
|
| 868 |
+
df_json = """
|
| 869 |
+
{
|
| 870 |
+
"schema":{
|
| 871 |
+
"fields":[
|
| 872 |
+
{"name":"index","type":"integer"},
|
| 873 |
+
{"name":"a","type":"string"}
|
| 874 |
+
],
|
| 875 |
+
"primaryKey":["index"],
|
| 876 |
+
"pandas_version":"0.20.0"
|
| 877 |
+
},
|
| 878 |
+
"data":[
|
| 879 |
+
{"index":0,"a":1},
|
| 880 |
+
{"index":1,"a":2.0},
|
| 881 |
+
{"index":2,"a":"s"}
|
| 882 |
+
]
|
| 883 |
+
}
|
| 884 |
+
"""
|
| 885 |
+
expected = DataFrame({"a": [1, 2.0, "s"]})
|
| 886 |
+
result = pd.read_json(StringIO(df_json), orient="table")
|
| 887 |
+
tm.assert_frame_equal(expected, result)
|
| 888 |
+
|
| 889 |
+
@pytest.mark.parametrize("freq", ["M", "2M", "Q", "2Q", "Y", "2Y"])
|
| 890 |
+
def test_read_json_table_orient_period_depr_freq(self, freq):
|
| 891 |
+
# GH#9586
|
| 892 |
+
df = DataFrame(
|
| 893 |
+
{"ints": [1, 2]},
|
| 894 |
+
index=pd.PeriodIndex(["2020-01", "2021-06"], freq=freq),
|
| 895 |
+
)
|
| 896 |
+
out = StringIO(df.to_json(orient="table"))
|
| 897 |
+
result = pd.read_json(out, orient="table")
|
| 898 |
+
tm.assert_frame_equal(df, result)
|
.venv/lib/python3.13/site-packages/pandas/tests/io/json/test_json_table_schema_ext_dtype.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for ExtensionDtype Table Schema integration."""
|
| 2 |
+
|
| 3 |
+
from collections import OrderedDict
|
| 4 |
+
import datetime as dt
|
| 5 |
+
import decimal
|
| 6 |
+
from io import StringIO
|
| 7 |
+
import json
|
| 8 |
+
|
| 9 |
+
import pytest
|
| 10 |
+
|
| 11 |
+
from pandas import (
|
| 12 |
+
NA,
|
| 13 |
+
DataFrame,
|
| 14 |
+
Index,
|
| 15 |
+
array,
|
| 16 |
+
read_json,
|
| 17 |
+
)
|
| 18 |
+
import pandas._testing as tm
|
| 19 |
+
from pandas.core.arrays.integer import Int64Dtype
|
| 20 |
+
from pandas.core.arrays.string_ import StringDtype
|
| 21 |
+
from pandas.core.series import Series
|
| 22 |
+
from pandas.tests.extension.date import (
|
| 23 |
+
DateArray,
|
| 24 |
+
DateDtype,
|
| 25 |
+
)
|
| 26 |
+
from pandas.tests.extension.decimal.array import (
|
| 27 |
+
DecimalArray,
|
| 28 |
+
DecimalDtype,
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
from pandas.io.json._table_schema import (
|
| 32 |
+
as_json_table_type,
|
| 33 |
+
build_table_schema,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class TestBuildSchema:
|
| 38 |
+
def test_build_table_schema(self):
|
| 39 |
+
df = DataFrame(
|
| 40 |
+
{
|
| 41 |
+
"A": DateArray([dt.date(2021, 10, 10)]),
|
| 42 |
+
"B": DecimalArray([decimal.Decimal(10)]),
|
| 43 |
+
"C": array(["pandas"], dtype="string"),
|
| 44 |
+
"D": array([10], dtype="Int64"),
|
| 45 |
+
}
|
| 46 |
+
)
|
| 47 |
+
result = build_table_schema(df, version=False)
|
| 48 |
+
expected = {
|
| 49 |
+
"fields": [
|
| 50 |
+
{"name": "index", "type": "integer"},
|
| 51 |
+
{"name": "A", "type": "any", "extDtype": "DateDtype"},
|
| 52 |
+
{"name": "B", "type": "number", "extDtype": "decimal"},
|
| 53 |
+
{"name": "C", "type": "string", "extDtype": "string"},
|
| 54 |
+
{"name": "D", "type": "integer", "extDtype": "Int64"},
|
| 55 |
+
],
|
| 56 |
+
"primaryKey": ["index"],
|
| 57 |
+
}
|
| 58 |
+
assert result == expected
|
| 59 |
+
result = build_table_schema(df)
|
| 60 |
+
assert "pandas_version" in result
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class TestTableSchemaType:
|
| 64 |
+
@pytest.mark.parametrize("box", [lambda x: x, Series])
|
| 65 |
+
def test_as_json_table_type_ext_date_array_dtype(self, box):
|
| 66 |
+
date_data = box(DateArray([dt.date(2021, 10, 10)]))
|
| 67 |
+
assert as_json_table_type(date_data.dtype) == "any"
|
| 68 |
+
|
| 69 |
+
def test_as_json_table_type_ext_date_dtype(self):
|
| 70 |
+
assert as_json_table_type(DateDtype()) == "any"
|
| 71 |
+
|
| 72 |
+
@pytest.mark.parametrize("box", [lambda x: x, Series])
|
| 73 |
+
def test_as_json_table_type_ext_decimal_array_dtype(self, box):
|
| 74 |
+
decimal_data = box(DecimalArray([decimal.Decimal(10)]))
|
| 75 |
+
assert as_json_table_type(decimal_data.dtype) == "number"
|
| 76 |
+
|
| 77 |
+
def test_as_json_table_type_ext_decimal_dtype(self):
|
| 78 |
+
assert as_json_table_type(DecimalDtype()) == "number"
|
| 79 |
+
|
| 80 |
+
@pytest.mark.parametrize("box", [lambda x: x, Series])
|
| 81 |
+
def test_as_json_table_type_ext_string_array_dtype(self, box):
|
| 82 |
+
string_data = box(array(["pandas"], dtype="string"))
|
| 83 |
+
assert as_json_table_type(string_data.dtype) == "string"
|
| 84 |
+
|
| 85 |
+
def test_as_json_table_type_ext_string_dtype(self):
|
| 86 |
+
assert as_json_table_type(StringDtype()) == "string"
|
| 87 |
+
|
| 88 |
+
@pytest.mark.parametrize("box", [lambda x: x, Series])
|
| 89 |
+
def test_as_json_table_type_ext_integer_array_dtype(self, box):
|
| 90 |
+
integer_data = box(array([10], dtype="Int64"))
|
| 91 |
+
assert as_json_table_type(integer_data.dtype) == "integer"
|
| 92 |
+
|
| 93 |
+
def test_as_json_table_type_ext_integer_dtype(self):
|
| 94 |
+
assert as_json_table_type(Int64Dtype()) == "integer"
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
class TestTableOrient:
|
| 98 |
+
@pytest.fixture
|
| 99 |
+
def da(self):
|
| 100 |
+
"""Fixture for creating a DateArray."""
|
| 101 |
+
return DateArray([dt.date(2021, 10, 10)])
|
| 102 |
+
|
| 103 |
+
@pytest.fixture
|
| 104 |
+
def dc(self):
|
| 105 |
+
"""Fixture for creating a DecimalArray."""
|
| 106 |
+
return DecimalArray([decimal.Decimal(10)])
|
| 107 |
+
|
| 108 |
+
@pytest.fixture
|
| 109 |
+
def sa(self):
|
| 110 |
+
"""Fixture for creating a StringDtype array."""
|
| 111 |
+
return array(["pandas"], dtype="string")
|
| 112 |
+
|
| 113 |
+
@pytest.fixture
|
| 114 |
+
def ia(self):
|
| 115 |
+
"""Fixture for creating an Int64Dtype array."""
|
| 116 |
+
return array([10], dtype="Int64")
|
| 117 |
+
|
| 118 |
+
def test_build_date_series(self, da):
|
| 119 |
+
s = Series(da, name="a")
|
| 120 |
+
s.index.name = "id"
|
| 121 |
+
result = s.to_json(orient="table", date_format="iso")
|
| 122 |
+
result = json.loads(result, object_pairs_hook=OrderedDict)
|
| 123 |
+
|
| 124 |
+
assert "pandas_version" in result["schema"]
|
| 125 |
+
result["schema"].pop("pandas_version")
|
| 126 |
+
|
| 127 |
+
fields = [
|
| 128 |
+
{"name": "id", "type": "integer"},
|
| 129 |
+
{"name": "a", "type": "any", "extDtype": "DateDtype"},
|
| 130 |
+
]
|
| 131 |
+
|
| 132 |
+
schema = {"fields": fields, "primaryKey": ["id"]}
|
| 133 |
+
|
| 134 |
+
expected = OrderedDict(
|
| 135 |
+
[
|
| 136 |
+
("schema", schema),
|
| 137 |
+
("data", [OrderedDict([("id", 0), ("a", "2021-10-10T00:00:00.000")])]),
|
| 138 |
+
]
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
assert result == expected
|
| 142 |
+
|
| 143 |
+
def test_build_decimal_series(self, dc):
|
| 144 |
+
s = Series(dc, name="a")
|
| 145 |
+
s.index.name = "id"
|
| 146 |
+
result = s.to_json(orient="table", date_format="iso")
|
| 147 |
+
result = json.loads(result, object_pairs_hook=OrderedDict)
|
| 148 |
+
|
| 149 |
+
assert "pandas_version" in result["schema"]
|
| 150 |
+
result["schema"].pop("pandas_version")
|
| 151 |
+
|
| 152 |
+
fields = [
|
| 153 |
+
{"name": "id", "type": "integer"},
|
| 154 |
+
{"name": "a", "type": "number", "extDtype": "decimal"},
|
| 155 |
+
]
|
| 156 |
+
|
| 157 |
+
schema = {"fields": fields, "primaryKey": ["id"]}
|
| 158 |
+
|
| 159 |
+
expected = OrderedDict(
|
| 160 |
+
[
|
| 161 |
+
("schema", schema),
|
| 162 |
+
("data", [OrderedDict([("id", 0), ("a", "10")])]),
|
| 163 |
+
]
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
assert result == expected
|
| 167 |
+
|
| 168 |
+
def test_build_string_series(self, sa):
|
| 169 |
+
s = Series(sa, name="a")
|
| 170 |
+
s.index.name = "id"
|
| 171 |
+
result = s.to_json(orient="table", date_format="iso")
|
| 172 |
+
result = json.loads(result, object_pairs_hook=OrderedDict)
|
| 173 |
+
|
| 174 |
+
assert "pandas_version" in result["schema"]
|
| 175 |
+
result["schema"].pop("pandas_version")
|
| 176 |
+
|
| 177 |
+
fields = [
|
| 178 |
+
{"name": "id", "type": "integer"},
|
| 179 |
+
{"name": "a", "type": "string", "extDtype": "string"},
|
| 180 |
+
]
|
| 181 |
+
|
| 182 |
+
schema = {"fields": fields, "primaryKey": ["id"]}
|
| 183 |
+
|
| 184 |
+
expected = OrderedDict(
|
| 185 |
+
[
|
| 186 |
+
("schema", schema),
|
| 187 |
+
("data", [OrderedDict([("id", 0), ("a", "pandas")])]),
|
| 188 |
+
]
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
assert result == expected
|
| 192 |
+
|
| 193 |
+
def test_build_int64_series(self, ia):
|
| 194 |
+
s = Series(ia, name="a")
|
| 195 |
+
s.index.name = "id"
|
| 196 |
+
result = s.to_json(orient="table", date_format="iso")
|
| 197 |
+
result = json.loads(result, object_pairs_hook=OrderedDict)
|
| 198 |
+
|
| 199 |
+
assert "pandas_version" in result["schema"]
|
| 200 |
+
result["schema"].pop("pandas_version")
|
| 201 |
+
|
| 202 |
+
fields = [
|
| 203 |
+
{"name": "id", "type": "integer"},
|
| 204 |
+
{"name": "a", "type": "integer", "extDtype": "Int64"},
|
| 205 |
+
]
|
| 206 |
+
|
| 207 |
+
schema = {"fields": fields, "primaryKey": ["id"]}
|
| 208 |
+
|
| 209 |
+
expected = OrderedDict(
|
| 210 |
+
[
|
| 211 |
+
("schema", schema),
|
| 212 |
+
("data", [OrderedDict([("id", 0), ("a", 10)])]),
|
| 213 |
+
]
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
assert result == expected
|
| 217 |
+
|
| 218 |
+
def test_to_json(self, da, dc, sa, ia):
|
| 219 |
+
df = DataFrame(
|
| 220 |
+
{
|
| 221 |
+
"A": da,
|
| 222 |
+
"B": dc,
|
| 223 |
+
"C": sa,
|
| 224 |
+
"D": ia,
|
| 225 |
+
}
|
| 226 |
+
)
|
| 227 |
+
df.index.name = "idx"
|
| 228 |
+
result = df.to_json(orient="table", date_format="iso")
|
| 229 |
+
result = json.loads(result, object_pairs_hook=OrderedDict)
|
| 230 |
+
|
| 231 |
+
assert "pandas_version" in result["schema"]
|
| 232 |
+
result["schema"].pop("pandas_version")
|
| 233 |
+
|
| 234 |
+
fields = [
|
| 235 |
+
OrderedDict({"name": "idx", "type": "integer"}),
|
| 236 |
+
OrderedDict({"name": "A", "type": "any", "extDtype": "DateDtype"}),
|
| 237 |
+
OrderedDict({"name": "B", "type": "number", "extDtype": "decimal"}),
|
| 238 |
+
OrderedDict({"name": "C", "type": "string", "extDtype": "string"}),
|
| 239 |
+
OrderedDict({"name": "D", "type": "integer", "extDtype": "Int64"}),
|
| 240 |
+
]
|
| 241 |
+
|
| 242 |
+
schema = OrderedDict({"fields": fields, "primaryKey": ["idx"]})
|
| 243 |
+
data = [
|
| 244 |
+
OrderedDict(
|
| 245 |
+
[
|
| 246 |
+
("idx", 0),
|
| 247 |
+
("A", "2021-10-10T00:00:00.000"),
|
| 248 |
+
("B", "10"),
|
| 249 |
+
("C", "pandas"),
|
| 250 |
+
("D", 10),
|
| 251 |
+
]
|
| 252 |
+
)
|
| 253 |
+
]
|
| 254 |
+
expected = OrderedDict([("schema", schema), ("data", data)])
|
| 255 |
+
|
| 256 |
+
assert result == expected
|
| 257 |
+
|
| 258 |
+
def test_json_ext_dtype_reading_roundtrip(self):
|
| 259 |
+
# GH#40255
|
| 260 |
+
df = DataFrame(
|
| 261 |
+
{
|
| 262 |
+
"a": Series([2, NA], dtype="Int64"),
|
| 263 |
+
"b": Series([1.5, NA], dtype="Float64"),
|
| 264 |
+
"c": Series([True, NA], dtype="boolean"),
|
| 265 |
+
},
|
| 266 |
+
index=Index([1, NA], dtype="Int64"),
|
| 267 |
+
)
|
| 268 |
+
expected = df.copy()
|
| 269 |
+
data_json = df.to_json(orient="table", indent=4)
|
| 270 |
+
result = read_json(StringIO(data_json), orient="table")
|
| 271 |
+
tm.assert_frame_equal(result, expected)
|
| 272 |
+
|
| 273 |
+
def test_json_ext_dtype_reading(self):
|
| 274 |
+
# GH#40255
|
| 275 |
+
data_json = """{
|
| 276 |
+
"schema":{
|
| 277 |
+
"fields":[
|
| 278 |
+
{
|
| 279 |
+
"name":"a",
|
| 280 |
+
"type":"integer",
|
| 281 |
+
"extDtype":"Int64"
|
| 282 |
+
}
|
| 283 |
+
],
|
| 284 |
+
},
|
| 285 |
+
"data":[
|
| 286 |
+
{
|
| 287 |
+
"a":2
|
| 288 |
+
},
|
| 289 |
+
{
|
| 290 |
+
"a":null
|
| 291 |
+
}
|
| 292 |
+
]
|
| 293 |
+
}"""
|
| 294 |
+
result = read_json(StringIO(data_json), orient="table")
|
| 295 |
+
expected = DataFrame({"a": Series([2, NA], dtype="Int64")})
|
| 296 |
+
tm.assert_frame_equal(result, expected)
|
.venv/lib/python3.13/site-packages/pandas/tests/io/json/test_normalize.py
ADDED
|
@@ -0,0 +1,942 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
import pytest
|
| 5 |
+
|
| 6 |
+
from pandas import (
|
| 7 |
+
DataFrame,
|
| 8 |
+
Index,
|
| 9 |
+
Series,
|
| 10 |
+
json_normalize,
|
| 11 |
+
)
|
| 12 |
+
import pandas._testing as tm
|
| 13 |
+
|
| 14 |
+
from pandas.io.json._normalize import nested_to_record
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@pytest.fixture
|
| 18 |
+
def deep_nested():
|
| 19 |
+
# deeply nested data
|
| 20 |
+
return [
|
| 21 |
+
{
|
| 22 |
+
"country": "USA",
|
| 23 |
+
"states": [
|
| 24 |
+
{
|
| 25 |
+
"name": "California",
|
| 26 |
+
"cities": [
|
| 27 |
+
{"name": "San Francisco", "pop": 12345},
|
| 28 |
+
{"name": "Los Angeles", "pop": 12346},
|
| 29 |
+
],
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
"name": "Ohio",
|
| 33 |
+
"cities": [
|
| 34 |
+
{"name": "Columbus", "pop": 1234},
|
| 35 |
+
{"name": "Cleveland", "pop": 1236},
|
| 36 |
+
],
|
| 37 |
+
},
|
| 38 |
+
],
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"country": "Germany",
|
| 42 |
+
"states": [
|
| 43 |
+
{"name": "Bayern", "cities": [{"name": "Munich", "pop": 12347}]},
|
| 44 |
+
{
|
| 45 |
+
"name": "Nordrhein-Westfalen",
|
| 46 |
+
"cities": [
|
| 47 |
+
{"name": "Duesseldorf", "pop": 1238},
|
| 48 |
+
{"name": "Koeln", "pop": 1239},
|
| 49 |
+
],
|
| 50 |
+
},
|
| 51 |
+
],
|
| 52 |
+
},
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@pytest.fixture
|
| 57 |
+
def state_data():
|
| 58 |
+
return [
|
| 59 |
+
{
|
| 60 |
+
"counties": [
|
| 61 |
+
{"name": "Dade", "population": 12345},
|
| 62 |
+
{"name": "Broward", "population": 40000},
|
| 63 |
+
{"name": "Palm Beach", "population": 60000},
|
| 64 |
+
],
|
| 65 |
+
"info": {"governor": "Rick Scott"},
|
| 66 |
+
"shortname": "FL",
|
| 67 |
+
"state": "Florida",
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"counties": [
|
| 71 |
+
{"name": "Summit", "population": 1234},
|
| 72 |
+
{"name": "Cuyahoga", "population": 1337},
|
| 73 |
+
],
|
| 74 |
+
"info": {"governor": "John Kasich"},
|
| 75 |
+
"shortname": "OH",
|
| 76 |
+
"state": "Ohio",
|
| 77 |
+
},
|
| 78 |
+
]
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
@pytest.fixture
|
| 82 |
+
def missing_metadata():
|
| 83 |
+
return [
|
| 84 |
+
{
|
| 85 |
+
"name": "Alice",
|
| 86 |
+
"addresses": [
|
| 87 |
+
{
|
| 88 |
+
"number": 9562,
|
| 89 |
+
"street": "Morris St.",
|
| 90 |
+
"city": "Massillon",
|
| 91 |
+
"state": "OH",
|
| 92 |
+
"zip": 44646,
|
| 93 |
+
}
|
| 94 |
+
],
|
| 95 |
+
"previous_residences": {"cities": [{"city_name": "Foo York City"}]},
|
| 96 |
+
},
|
| 97 |
+
{
|
| 98 |
+
"addresses": [
|
| 99 |
+
{
|
| 100 |
+
"number": 8449,
|
| 101 |
+
"street": "Spring St.",
|
| 102 |
+
"city": "Elizabethton",
|
| 103 |
+
"state": "TN",
|
| 104 |
+
"zip": 37643,
|
| 105 |
+
}
|
| 106 |
+
],
|
| 107 |
+
"previous_residences": {"cities": [{"city_name": "Barmingham"}]},
|
| 108 |
+
},
|
| 109 |
+
]
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
class TestJSONNormalize:
|
| 113 |
+
def test_simple_records(self):
|
| 114 |
+
recs = [
|
| 115 |
+
{"a": 1, "b": 2, "c": 3},
|
| 116 |
+
{"a": 4, "b": 5, "c": 6},
|
| 117 |
+
{"a": 7, "b": 8, "c": 9},
|
| 118 |
+
{"a": 10, "b": 11, "c": 12},
|
| 119 |
+
]
|
| 120 |
+
|
| 121 |
+
result = json_normalize(recs)
|
| 122 |
+
expected = DataFrame(recs)
|
| 123 |
+
|
| 124 |
+
tm.assert_frame_equal(result, expected)
|
| 125 |
+
|
| 126 |
+
def test_simple_normalize(self, state_data):
|
| 127 |
+
result = json_normalize(state_data[0], "counties")
|
| 128 |
+
expected = DataFrame(state_data[0]["counties"])
|
| 129 |
+
tm.assert_frame_equal(result, expected)
|
| 130 |
+
|
| 131 |
+
result = json_normalize(state_data, "counties")
|
| 132 |
+
|
| 133 |
+
expected = []
|
| 134 |
+
for rec in state_data:
|
| 135 |
+
expected.extend(rec["counties"])
|
| 136 |
+
expected = DataFrame(expected)
|
| 137 |
+
|
| 138 |
+
tm.assert_frame_equal(result, expected)
|
| 139 |
+
|
| 140 |
+
result = json_normalize(state_data, "counties", meta="state")
|
| 141 |
+
expected["state"] = np.array(["Florida", "Ohio"]).repeat([3, 2])
|
| 142 |
+
|
| 143 |
+
tm.assert_frame_equal(result, expected)
|
| 144 |
+
|
| 145 |
+
def test_fields_list_type_normalize(self):
|
| 146 |
+
parse_metadata_fields_list_type = [
|
| 147 |
+
{"values": [1, 2, 3], "metadata": {"listdata": [1, 2]}}
|
| 148 |
+
]
|
| 149 |
+
result = json_normalize(
|
| 150 |
+
parse_metadata_fields_list_type,
|
| 151 |
+
record_path=["values"],
|
| 152 |
+
meta=[["metadata", "listdata"]],
|
| 153 |
+
)
|
| 154 |
+
expected = DataFrame(
|
| 155 |
+
{0: [1, 2, 3], "metadata.listdata": [[1, 2], [1, 2], [1, 2]]}
|
| 156 |
+
)
|
| 157 |
+
tm.assert_frame_equal(result, expected)
|
| 158 |
+
|
| 159 |
+
def test_empty_array(self):
|
| 160 |
+
result = json_normalize([])
|
| 161 |
+
expected = DataFrame()
|
| 162 |
+
tm.assert_frame_equal(result, expected)
|
| 163 |
+
|
| 164 |
+
@pytest.mark.parametrize(
|
| 165 |
+
"data, record_path, exception_type",
|
| 166 |
+
[
|
| 167 |
+
([{"a": 0}, {"a": 1}], None, None),
|
| 168 |
+
({"a": [{"a": 0}, {"a": 1}]}, "a", None),
|
| 169 |
+
('{"a": [{"a": 0}, {"a": 1}]}', None, NotImplementedError),
|
| 170 |
+
(None, None, NotImplementedError),
|
| 171 |
+
],
|
| 172 |
+
)
|
| 173 |
+
def test_accepted_input(self, data, record_path, exception_type):
|
| 174 |
+
if exception_type is not None:
|
| 175 |
+
with pytest.raises(exception_type, match="^$"):
|
| 176 |
+
json_normalize(data, record_path=record_path)
|
| 177 |
+
else:
|
| 178 |
+
result = json_normalize(data, record_path=record_path)
|
| 179 |
+
expected = DataFrame([0, 1], columns=["a"])
|
| 180 |
+
tm.assert_frame_equal(result, expected)
|
| 181 |
+
|
| 182 |
+
def test_simple_normalize_with_separator(self, deep_nested):
|
| 183 |
+
# GH 14883
|
| 184 |
+
result = json_normalize({"A": {"A": 1, "B": 2}})
|
| 185 |
+
expected = DataFrame([[1, 2]], columns=["A.A", "A.B"])
|
| 186 |
+
tm.assert_frame_equal(result.reindex_like(expected), expected)
|
| 187 |
+
|
| 188 |
+
result = json_normalize({"A": {"A": 1, "B": 2}}, sep="_")
|
| 189 |
+
expected = DataFrame([[1, 2]], columns=["A_A", "A_B"])
|
| 190 |
+
tm.assert_frame_equal(result.reindex_like(expected), expected)
|
| 191 |
+
|
| 192 |
+
result = json_normalize({"A": {"A": 1, "B": 2}}, sep="\u03c3")
|
| 193 |
+
expected = DataFrame([[1, 2]], columns=["A\u03c3A", "A\u03c3B"])
|
| 194 |
+
tm.assert_frame_equal(result.reindex_like(expected), expected)
|
| 195 |
+
|
| 196 |
+
result = json_normalize(
|
| 197 |
+
deep_nested,
|
| 198 |
+
["states", "cities"],
|
| 199 |
+
meta=["country", ["states", "name"]],
|
| 200 |
+
sep="_",
|
| 201 |
+
)
|
| 202 |
+
expected = Index(["name", "pop", "country", "states_name"]).sort_values()
|
| 203 |
+
assert result.columns.sort_values().equals(expected)
|
| 204 |
+
|
| 205 |
+
def test_normalize_with_multichar_separator(self):
|
| 206 |
+
# GH #43831
|
| 207 |
+
data = {"a": [1, 2], "b": {"b_1": 2, "b_2": (3, 4)}}
|
| 208 |
+
result = json_normalize(data, sep="__")
|
| 209 |
+
expected = DataFrame([[[1, 2], 2, (3, 4)]], columns=["a", "b__b_1", "b__b_2"])
|
| 210 |
+
tm.assert_frame_equal(result, expected)
|
| 211 |
+
|
| 212 |
+
def test_value_array_record_prefix(self):
|
| 213 |
+
# GH 21536
|
| 214 |
+
result = json_normalize({"A": [1, 2]}, "A", record_prefix="Prefix.")
|
| 215 |
+
expected = DataFrame([[1], [2]], columns=["Prefix.0"])
|
| 216 |
+
tm.assert_frame_equal(result, expected)
|
| 217 |
+
|
| 218 |
+
def test_nested_object_record_path(self):
|
| 219 |
+
# GH 22706
|
| 220 |
+
data = {
|
| 221 |
+
"state": "Florida",
|
| 222 |
+
"info": {
|
| 223 |
+
"governor": "Rick Scott",
|
| 224 |
+
"counties": [
|
| 225 |
+
{"name": "Dade", "population": 12345},
|
| 226 |
+
{"name": "Broward", "population": 40000},
|
| 227 |
+
{"name": "Palm Beach", "population": 60000},
|
| 228 |
+
],
|
| 229 |
+
},
|
| 230 |
+
}
|
| 231 |
+
result = json_normalize(data, record_path=["info", "counties"])
|
| 232 |
+
expected = DataFrame(
|
| 233 |
+
[["Dade", 12345], ["Broward", 40000], ["Palm Beach", 60000]],
|
| 234 |
+
columns=["name", "population"],
|
| 235 |
+
)
|
| 236 |
+
tm.assert_frame_equal(result, expected)
|
| 237 |
+
|
| 238 |
+
def test_more_deeply_nested(self, deep_nested):
|
| 239 |
+
result = json_normalize(
|
| 240 |
+
deep_nested, ["states", "cities"], meta=["country", ["states", "name"]]
|
| 241 |
+
)
|
| 242 |
+
ex_data = {
|
| 243 |
+
"country": ["USA"] * 4 + ["Germany"] * 3,
|
| 244 |
+
"states.name": [
|
| 245 |
+
"California",
|
| 246 |
+
"California",
|
| 247 |
+
"Ohio",
|
| 248 |
+
"Ohio",
|
| 249 |
+
"Bayern",
|
| 250 |
+
"Nordrhein-Westfalen",
|
| 251 |
+
"Nordrhein-Westfalen",
|
| 252 |
+
],
|
| 253 |
+
"name": [
|
| 254 |
+
"San Francisco",
|
| 255 |
+
"Los Angeles",
|
| 256 |
+
"Columbus",
|
| 257 |
+
"Cleveland",
|
| 258 |
+
"Munich",
|
| 259 |
+
"Duesseldorf",
|
| 260 |
+
"Koeln",
|
| 261 |
+
],
|
| 262 |
+
"pop": [12345, 12346, 1234, 1236, 12347, 1238, 1239],
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
expected = DataFrame(ex_data, columns=result.columns)
|
| 266 |
+
tm.assert_frame_equal(result, expected)
|
| 267 |
+
|
| 268 |
+
def test_shallow_nested(self):
|
| 269 |
+
data = [
|
| 270 |
+
{
|
| 271 |
+
"state": "Florida",
|
| 272 |
+
"shortname": "FL",
|
| 273 |
+
"info": {"governor": "Rick Scott"},
|
| 274 |
+
"counties": [
|
| 275 |
+
{"name": "Dade", "population": 12345},
|
| 276 |
+
{"name": "Broward", "population": 40000},
|
| 277 |
+
{"name": "Palm Beach", "population": 60000},
|
| 278 |
+
],
|
| 279 |
+
},
|
| 280 |
+
{
|
| 281 |
+
"state": "Ohio",
|
| 282 |
+
"shortname": "OH",
|
| 283 |
+
"info": {"governor": "John Kasich"},
|
| 284 |
+
"counties": [
|
| 285 |
+
{"name": "Summit", "population": 1234},
|
| 286 |
+
{"name": "Cuyahoga", "population": 1337},
|
| 287 |
+
],
|
| 288 |
+
},
|
| 289 |
+
]
|
| 290 |
+
|
| 291 |
+
result = json_normalize(
|
| 292 |
+
data, "counties", ["state", "shortname", ["info", "governor"]]
|
| 293 |
+
)
|
| 294 |
+
ex_data = {
|
| 295 |
+
"name": ["Dade", "Broward", "Palm Beach", "Summit", "Cuyahoga"],
|
| 296 |
+
"state": ["Florida"] * 3 + ["Ohio"] * 2,
|
| 297 |
+
"shortname": ["FL", "FL", "FL", "OH", "OH"],
|
| 298 |
+
"info.governor": ["Rick Scott"] * 3 + ["John Kasich"] * 2,
|
| 299 |
+
"population": [12345, 40000, 60000, 1234, 1337],
|
| 300 |
+
}
|
| 301 |
+
expected = DataFrame(ex_data, columns=result.columns)
|
| 302 |
+
tm.assert_frame_equal(result, expected)
|
| 303 |
+
|
| 304 |
+
def test_nested_meta_path_with_nested_record_path(self, state_data):
|
| 305 |
+
# GH 27220
|
| 306 |
+
result = json_normalize(
|
| 307 |
+
data=state_data,
|
| 308 |
+
record_path=["counties"],
|
| 309 |
+
meta=["state", "shortname", ["info", "governor"]],
|
| 310 |
+
errors="ignore",
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
ex_data = {
|
| 314 |
+
"name": ["Dade", "Broward", "Palm Beach", "Summit", "Cuyahoga"],
|
| 315 |
+
"population": [12345, 40000, 60000, 1234, 1337],
|
| 316 |
+
"state": ["Florida"] * 3 + ["Ohio"] * 2,
|
| 317 |
+
"shortname": ["FL"] * 3 + ["OH"] * 2,
|
| 318 |
+
"info.governor": ["Rick Scott"] * 3 + ["John Kasich"] * 2,
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
expected = DataFrame(ex_data)
|
| 322 |
+
tm.assert_frame_equal(result, expected)
|
| 323 |
+
|
| 324 |
+
def test_meta_name_conflict(self):
|
| 325 |
+
data = [
|
| 326 |
+
{
|
| 327 |
+
"foo": "hello",
|
| 328 |
+
"bar": "there",
|
| 329 |
+
"data": [
|
| 330 |
+
{"foo": "something", "bar": "else"},
|
| 331 |
+
{"foo": "something2", "bar": "else2"},
|
| 332 |
+
],
|
| 333 |
+
}
|
| 334 |
+
]
|
| 335 |
+
|
| 336 |
+
msg = r"Conflicting metadata name (foo|bar), need distinguishing prefix"
|
| 337 |
+
with pytest.raises(ValueError, match=msg):
|
| 338 |
+
json_normalize(data, "data", meta=["foo", "bar"])
|
| 339 |
+
|
| 340 |
+
result = json_normalize(data, "data", meta=["foo", "bar"], meta_prefix="meta")
|
| 341 |
+
|
| 342 |
+
for val in ["metafoo", "metabar", "foo", "bar"]:
|
| 343 |
+
assert val in result
|
| 344 |
+
|
| 345 |
+
def test_meta_parameter_not_modified(self):
|
| 346 |
+
# GH 18610
|
| 347 |
+
data = [
|
| 348 |
+
{
|
| 349 |
+
"foo": "hello",
|
| 350 |
+
"bar": "there",
|
| 351 |
+
"data": [
|
| 352 |
+
{"foo": "something", "bar": "else"},
|
| 353 |
+
{"foo": "something2", "bar": "else2"},
|
| 354 |
+
],
|
| 355 |
+
}
|
| 356 |
+
]
|
| 357 |
+
|
| 358 |
+
COLUMNS = ["foo", "bar"]
|
| 359 |
+
result = json_normalize(data, "data", meta=COLUMNS, meta_prefix="meta")
|
| 360 |
+
|
| 361 |
+
assert COLUMNS == ["foo", "bar"]
|
| 362 |
+
for val in ["metafoo", "metabar", "foo", "bar"]:
|
| 363 |
+
assert val in result
|
| 364 |
+
|
| 365 |
+
def test_record_prefix(self, state_data):
|
| 366 |
+
result = json_normalize(state_data[0], "counties")
|
| 367 |
+
expected = DataFrame(state_data[0]["counties"])
|
| 368 |
+
tm.assert_frame_equal(result, expected)
|
| 369 |
+
|
| 370 |
+
result = json_normalize(
|
| 371 |
+
state_data, "counties", meta="state", record_prefix="county_"
|
| 372 |
+
)
|
| 373 |
+
|
| 374 |
+
expected = []
|
| 375 |
+
for rec in state_data:
|
| 376 |
+
expected.extend(rec["counties"])
|
| 377 |
+
expected = DataFrame(expected)
|
| 378 |
+
expected = expected.rename(columns=lambda x: "county_" + x)
|
| 379 |
+
expected["state"] = np.array(["Florida", "Ohio"]).repeat([3, 2])
|
| 380 |
+
|
| 381 |
+
tm.assert_frame_equal(result, expected)
|
| 382 |
+
|
| 383 |
+
def test_record_prefix_no_record_path_series(self):
|
| 384 |
+
# Ensure record_prefix is applied when record_path is None for Series input
|
| 385 |
+
s = Series([{"k": f"{i}", "m": "q"} for i in range(3)])
|
| 386 |
+
result = json_normalize(s, record_prefix="T.")
|
| 387 |
+
expected = DataFrame(
|
| 388 |
+
[
|
| 389 |
+
{"T.k": "0", "T.m": "q"},
|
| 390 |
+
{"T.k": "1", "T.m": "q"},
|
| 391 |
+
{"T.k": "2", "T.m": "q"},
|
| 392 |
+
]
|
| 393 |
+
)
|
| 394 |
+
tm.assert_frame_equal(result, expected)
|
| 395 |
+
|
| 396 |
+
def test_non_ascii_key(self):
|
| 397 |
+
testjson = (
|
| 398 |
+
b'[{"\xc3\x9cnic\xc3\xb8de":0,"sub":{"A":1, "B":2}},'
|
| 399 |
+
b'{"\xc3\x9cnic\xc3\xb8de":1,"sub":{"A":3, "B":4}}]'
|
| 400 |
+
).decode("utf8")
|
| 401 |
+
|
| 402 |
+
testdata = {
|
| 403 |
+
b"\xc3\x9cnic\xc3\xb8de".decode("utf8"): [0, 1],
|
| 404 |
+
"sub.A": [1, 3],
|
| 405 |
+
"sub.B": [2, 4],
|
| 406 |
+
}
|
| 407 |
+
expected = DataFrame(testdata)
|
| 408 |
+
|
| 409 |
+
result = json_normalize(json.loads(testjson))
|
| 410 |
+
tm.assert_frame_equal(result, expected)
|
| 411 |
+
|
| 412 |
+
def test_missing_field(self):
|
| 413 |
+
# GH20030:
|
| 414 |
+
author_missing_data = [
|
| 415 |
+
{"info": None},
|
| 416 |
+
{
|
| 417 |
+
"info": {"created_at": "11/08/1993", "last_updated": "26/05/2012"},
|
| 418 |
+
"author_name": {"first": "Jane", "last_name": "Doe"},
|
| 419 |
+
},
|
| 420 |
+
]
|
| 421 |
+
result = json_normalize(author_missing_data)
|
| 422 |
+
ex_data = [
|
| 423 |
+
{
|
| 424 |
+
"info": np.nan,
|
| 425 |
+
"info.created_at": np.nan,
|
| 426 |
+
"info.last_updated": np.nan,
|
| 427 |
+
"author_name.first": np.nan,
|
| 428 |
+
"author_name.last_name": np.nan,
|
| 429 |
+
},
|
| 430 |
+
{
|
| 431 |
+
"info": None,
|
| 432 |
+
"info.created_at": "11/08/1993",
|
| 433 |
+
"info.last_updated": "26/05/2012",
|
| 434 |
+
"author_name.first": "Jane",
|
| 435 |
+
"author_name.last_name": "Doe",
|
| 436 |
+
},
|
| 437 |
+
]
|
| 438 |
+
expected = DataFrame(ex_data)
|
| 439 |
+
tm.assert_frame_equal(result, expected)
|
| 440 |
+
|
| 441 |
+
@pytest.mark.parametrize(
|
| 442 |
+
"max_level,expected",
|
| 443 |
+
[
|
| 444 |
+
(
|
| 445 |
+
0,
|
| 446 |
+
[
|
| 447 |
+
{
|
| 448 |
+
"TextField": "Some text",
|
| 449 |
+
"UserField": {"Id": "ID001", "Name": "Name001"},
|
| 450 |
+
"CreatedBy": {"Name": "User001"},
|
| 451 |
+
"Image": {"a": "b"},
|
| 452 |
+
},
|
| 453 |
+
{
|
| 454 |
+
"TextField": "Some text",
|
| 455 |
+
"UserField": {"Id": "ID001", "Name": "Name001"},
|
| 456 |
+
"CreatedBy": {"Name": "User001"},
|
| 457 |
+
"Image": {"a": "b"},
|
| 458 |
+
},
|
| 459 |
+
],
|
| 460 |
+
),
|
| 461 |
+
(
|
| 462 |
+
1,
|
| 463 |
+
[
|
| 464 |
+
{
|
| 465 |
+
"TextField": "Some text",
|
| 466 |
+
"UserField.Id": "ID001",
|
| 467 |
+
"UserField.Name": "Name001",
|
| 468 |
+
"CreatedBy": {"Name": "User001"},
|
| 469 |
+
"Image": {"a": "b"},
|
| 470 |
+
},
|
| 471 |
+
{
|
| 472 |
+
"TextField": "Some text",
|
| 473 |
+
"UserField.Id": "ID001",
|
| 474 |
+
"UserField.Name": "Name001",
|
| 475 |
+
"CreatedBy": {"Name": "User001"},
|
| 476 |
+
"Image": {"a": "b"},
|
| 477 |
+
},
|
| 478 |
+
],
|
| 479 |
+
),
|
| 480 |
+
],
|
| 481 |
+
)
|
| 482 |
+
def test_max_level_with_records_path(self, max_level, expected):
|
| 483 |
+
# GH23843: Enhanced JSON normalize
|
| 484 |
+
test_input = [
|
| 485 |
+
{
|
| 486 |
+
"CreatedBy": {"Name": "User001"},
|
| 487 |
+
"Lookup": [
|
| 488 |
+
{
|
| 489 |
+
"TextField": "Some text",
|
| 490 |
+
"UserField": {"Id": "ID001", "Name": "Name001"},
|
| 491 |
+
},
|
| 492 |
+
{
|
| 493 |
+
"TextField": "Some text",
|
| 494 |
+
"UserField": {"Id": "ID001", "Name": "Name001"},
|
| 495 |
+
},
|
| 496 |
+
],
|
| 497 |
+
"Image": {"a": "b"},
|
| 498 |
+
"tags": [
|
| 499 |
+
{"foo": "something", "bar": "else"},
|
| 500 |
+
{"foo": "something2", "bar": "else2"},
|
| 501 |
+
],
|
| 502 |
+
}
|
| 503 |
+
]
|
| 504 |
+
|
| 505 |
+
result = json_normalize(
|
| 506 |
+
test_input,
|
| 507 |
+
record_path=["Lookup"],
|
| 508 |
+
meta=[["CreatedBy"], ["Image"]],
|
| 509 |
+
max_level=max_level,
|
| 510 |
+
)
|
| 511 |
+
expected_df = DataFrame(data=expected, columns=result.columns.values)
|
| 512 |
+
tm.assert_equal(expected_df, result)
|
| 513 |
+
|
| 514 |
+
@pytest.mark.parametrize("max_level", [0, None])
|
| 515 |
+
def test_json_normalize_non_dict_items(self, max_level):
|
| 516 |
+
# gh-62829, gh-64188
|
| 517 |
+
data_list = [np.nan, {"id": 12}, {"id": 13}]
|
| 518 |
+
|
| 519 |
+
result = json_normalize(data_list, max_level=max_level)
|
| 520 |
+
expected = DataFrame({"id": [np.nan, 12, 13]})
|
| 521 |
+
tm.assert_frame_equal(result, expected)
|
| 522 |
+
|
| 523 |
+
msg = "All items in data must be of type dict or NA-like, found float"
|
| 524 |
+
with pytest.raises(TypeError, match=msg):
|
| 525 |
+
json_normalize([1.0, {"id": 12}], max_level=max_level)
|
| 526 |
+
|
| 527 |
+
def test_nested_flattening_consistent(self):
|
| 528 |
+
# see gh-21537
|
| 529 |
+
df1 = json_normalize([{"A": {"B": 1}}])
|
| 530 |
+
df2 = json_normalize({"dummy": [{"A": {"B": 1}}]}, "dummy")
|
| 531 |
+
|
| 532 |
+
# They should be the same.
|
| 533 |
+
tm.assert_frame_equal(df1, df2)
|
| 534 |
+
|
| 535 |
+
def test_nonetype_record_path(self, nulls_fixture):
|
| 536 |
+
# see gh-30148
|
| 537 |
+
# should not raise TypeError
|
| 538 |
+
result = json_normalize(
|
| 539 |
+
[
|
| 540 |
+
{"state": "Texas", "info": nulls_fixture},
|
| 541 |
+
{"state": "Florida", "info": [{"i": 2}]},
|
| 542 |
+
],
|
| 543 |
+
record_path=["info"],
|
| 544 |
+
)
|
| 545 |
+
expected = DataFrame({"i": 2}, index=range(1))
|
| 546 |
+
tm.assert_equal(result, expected)
|
| 547 |
+
|
| 548 |
+
@pytest.mark.parametrize("value", ["false", "true", "{}", "1", '"text"'])
|
| 549 |
+
def test_non_list_record_path_errors(self, value):
|
| 550 |
+
# see gh-30148, GH 26284
|
| 551 |
+
parsed_value = json.loads(value)
|
| 552 |
+
test_input = {"state": "Texas", "info": parsed_value}
|
| 553 |
+
test_path = "info"
|
| 554 |
+
msg = (
|
| 555 |
+
f"Path must contain list or null, "
|
| 556 |
+
f"but got {type(parsed_value).__name__} at 'info'"
|
| 557 |
+
)
|
| 558 |
+
with pytest.raises(TypeError, match=msg):
|
| 559 |
+
json_normalize([test_input], record_path=[test_path])
|
| 560 |
+
|
| 561 |
+
def test_meta_non_iterable(self):
|
| 562 |
+
# GH 31507
|
| 563 |
+
data = """[{"id": 99, "data": [{"one": 1, "two": 2}]}]"""
|
| 564 |
+
|
| 565 |
+
result = json_normalize(json.loads(data), record_path=["data"], meta=["id"])
|
| 566 |
+
expected = DataFrame(
|
| 567 |
+
{"one": [1], "two": [2], "id": np.array([99], dtype=object)}
|
| 568 |
+
)
|
| 569 |
+
tm.assert_frame_equal(result, expected)
|
| 570 |
+
|
| 571 |
+
def test_generator(self, state_data):
|
| 572 |
+
# GH35923 Fix pd.json_normalize to not skip the first element of a
|
| 573 |
+
# generator input
|
| 574 |
+
def generator_data():
|
| 575 |
+
yield from state_data[0]["counties"]
|
| 576 |
+
|
| 577 |
+
result = json_normalize(generator_data())
|
| 578 |
+
expected = DataFrame(state_data[0]["counties"])
|
| 579 |
+
|
| 580 |
+
tm.assert_frame_equal(result, expected)
|
| 581 |
+
|
| 582 |
+
def test_top_column_with_leading_underscore(self):
|
| 583 |
+
# 49861
|
| 584 |
+
data = {"_id": {"a1": 10, "l2": {"l3": 0}}, "gg": 4}
|
| 585 |
+
result = json_normalize(data, sep="_")
|
| 586 |
+
expected = DataFrame([[4, 10, 0]], columns=["gg", "_id_a1", "_id_l2_l3"])
|
| 587 |
+
|
| 588 |
+
tm.assert_frame_equal(result, expected)
|
| 589 |
+
|
| 590 |
+
def test_series_index(self, state_data):
|
| 591 |
+
idx = Index([7, 8])
|
| 592 |
+
series = Series(state_data, index=idx)
|
| 593 |
+
result = json_normalize(series)
|
| 594 |
+
tm.assert_index_equal(result.index, idx)
|
| 595 |
+
result = json_normalize(series, "counties")
|
| 596 |
+
tm.assert_index_equal(result.index, idx.repeat([3, 2]))
|
| 597 |
+
|
| 598 |
+
|
| 599 |
+
class TestNestedToRecord:
|
| 600 |
+
def test_flat_stays_flat(self):
|
| 601 |
+
recs = [{"flat1": 1, "flat2": 2}, {"flat3": 3, "flat2": 4}]
|
| 602 |
+
result = nested_to_record(recs)
|
| 603 |
+
expected = recs
|
| 604 |
+
assert result == expected
|
| 605 |
+
|
| 606 |
+
def test_one_level_deep_flattens(self):
|
| 607 |
+
data = {"flat1": 1, "dict1": {"c": 1, "d": 2}}
|
| 608 |
+
|
| 609 |
+
result = nested_to_record(data)
|
| 610 |
+
expected = {"dict1.c": 1, "dict1.d": 2, "flat1": 1}
|
| 611 |
+
|
| 612 |
+
assert result == expected
|
| 613 |
+
|
| 614 |
+
def test_nested_flattens(self):
|
| 615 |
+
data = {
|
| 616 |
+
"flat1": 1,
|
| 617 |
+
"dict1": {"c": 1, "d": 2},
|
| 618 |
+
"nested": {"e": {"c": 1, "d": 2}, "d": 2},
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
result = nested_to_record(data)
|
| 622 |
+
expected = {
|
| 623 |
+
"dict1.c": 1,
|
| 624 |
+
"dict1.d": 2,
|
| 625 |
+
"flat1": 1,
|
| 626 |
+
"nested.d": 2,
|
| 627 |
+
"nested.e.c": 1,
|
| 628 |
+
"nested.e.d": 2,
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
assert result == expected
|
| 632 |
+
|
| 633 |
+
def test_json_normalize_errors(self, missing_metadata):
|
| 634 |
+
# GH14583:
|
| 635 |
+
# If meta keys are not always present a new option to set
|
| 636 |
+
# errors='ignore' has been implemented
|
| 637 |
+
|
| 638 |
+
msg = (
|
| 639 |
+
"Key 'name' not found. To replace missing values of "
|
| 640 |
+
"'name' with np.nan, pass in errors='ignore'"
|
| 641 |
+
)
|
| 642 |
+
with pytest.raises(KeyError, match=msg):
|
| 643 |
+
json_normalize(
|
| 644 |
+
data=missing_metadata,
|
| 645 |
+
record_path="addresses",
|
| 646 |
+
meta="name",
|
| 647 |
+
errors="raise",
|
| 648 |
+
)
|
| 649 |
+
|
| 650 |
+
def test_missing_meta(self, missing_metadata):
|
| 651 |
+
# GH25468
|
| 652 |
+
# If metadata is nullable with errors set to ignore, the null values
|
| 653 |
+
# should be numpy.nan values
|
| 654 |
+
result = json_normalize(
|
| 655 |
+
data=missing_metadata, record_path="addresses", meta="name", errors="ignore"
|
| 656 |
+
)
|
| 657 |
+
ex_data = [
|
| 658 |
+
[9562, "Morris St.", "Massillon", "OH", 44646, "Alice"],
|
| 659 |
+
[8449, "Spring St.", "Elizabethton", "TN", 37643, np.nan],
|
| 660 |
+
]
|
| 661 |
+
columns = ["number", "street", "city", "state", "zip", "name"]
|
| 662 |
+
expected = DataFrame(ex_data, columns=columns)
|
| 663 |
+
tm.assert_frame_equal(result, expected)
|
| 664 |
+
|
| 665 |
+
def test_missing_nested_meta(self):
|
| 666 |
+
# GH44312
|
| 667 |
+
# If errors="ignore" and nested metadata is null, we should return nan
|
| 668 |
+
data = {"meta": "foo", "nested_meta": None, "value": [{"rec": 1}, {"rec": 2}]}
|
| 669 |
+
result = json_normalize(
|
| 670 |
+
data,
|
| 671 |
+
record_path="value",
|
| 672 |
+
meta=["meta", ["nested_meta", "leaf"]],
|
| 673 |
+
errors="ignore",
|
| 674 |
+
)
|
| 675 |
+
ex_data = [[1, "foo", np.nan], [2, "foo", np.nan]]
|
| 676 |
+
columns = ["rec", "meta", "nested_meta.leaf"]
|
| 677 |
+
expected = DataFrame(ex_data, columns=columns).astype(
|
| 678 |
+
{"nested_meta.leaf": object}
|
| 679 |
+
)
|
| 680 |
+
tm.assert_frame_equal(result, expected)
|
| 681 |
+
|
| 682 |
+
# If errors="raise" and nested metadata is null, we should raise with the
|
| 683 |
+
# key of the first missing level
|
| 684 |
+
with pytest.raises(KeyError, match="'leaf' not found"):
|
| 685 |
+
json_normalize(
|
| 686 |
+
data,
|
| 687 |
+
record_path="value",
|
| 688 |
+
meta=["meta", ["nested_meta", "leaf"]],
|
| 689 |
+
errors="raise",
|
| 690 |
+
)
|
| 691 |
+
|
| 692 |
+
def test_missing_meta_multilevel_record_path_errors_raise(self, missing_metadata):
|
| 693 |
+
# GH41876
|
| 694 |
+
# Ensure errors='raise' works as intended even when a record_path of length
|
| 695 |
+
# greater than one is passed in
|
| 696 |
+
msg = (
|
| 697 |
+
"Key 'name' not found. To replace missing values of "
|
| 698 |
+
"'name' with np.nan, pass in errors='ignore'"
|
| 699 |
+
)
|
| 700 |
+
with pytest.raises(KeyError, match=msg):
|
| 701 |
+
json_normalize(
|
| 702 |
+
data=missing_metadata,
|
| 703 |
+
record_path=["previous_residences", "cities"],
|
| 704 |
+
meta="name",
|
| 705 |
+
errors="raise",
|
| 706 |
+
)
|
| 707 |
+
|
| 708 |
+
def test_missing_meta_multilevel_record_path_errors_ignore(self, missing_metadata):
|
| 709 |
+
# GH41876
|
| 710 |
+
# Ensure errors='ignore' works as intended even when a record_path of length
|
| 711 |
+
# greater than one is passed in
|
| 712 |
+
result = json_normalize(
|
| 713 |
+
data=missing_metadata,
|
| 714 |
+
record_path=["previous_residences", "cities"],
|
| 715 |
+
meta="name",
|
| 716 |
+
errors="ignore",
|
| 717 |
+
)
|
| 718 |
+
ex_data = [
|
| 719 |
+
["Foo York City", "Alice"],
|
| 720 |
+
["Barmingham", np.nan],
|
| 721 |
+
]
|
| 722 |
+
columns = ["city_name", "name"]
|
| 723 |
+
expected = DataFrame(ex_data, columns=columns)
|
| 724 |
+
tm.assert_frame_equal(result, expected)
|
| 725 |
+
|
| 726 |
+
def test_donot_drop_nonevalues(self):
|
| 727 |
+
# GH21356
|
| 728 |
+
data = [
|
| 729 |
+
{"info": None, "author_name": {"first": "Smith", "last_name": "Appleseed"}},
|
| 730 |
+
{
|
| 731 |
+
"info": {"created_at": "11/08/1993", "last_updated": "26/05/2012"},
|
| 732 |
+
"author_name": {"first": "Jane", "last_name": "Doe"},
|
| 733 |
+
},
|
| 734 |
+
]
|
| 735 |
+
result = nested_to_record(data)
|
| 736 |
+
expected = [
|
| 737 |
+
{
|
| 738 |
+
"info": None,
|
| 739 |
+
"author_name.first": "Smith",
|
| 740 |
+
"author_name.last_name": "Appleseed",
|
| 741 |
+
},
|
| 742 |
+
{
|
| 743 |
+
"author_name.first": "Jane",
|
| 744 |
+
"author_name.last_name": "Doe",
|
| 745 |
+
"info.created_at": "11/08/1993",
|
| 746 |
+
"info.last_updated": "26/05/2012",
|
| 747 |
+
},
|
| 748 |
+
]
|
| 749 |
+
|
| 750 |
+
assert result == expected
|
| 751 |
+
|
| 752 |
+
def test_nonetype_top_level_bottom_level(self):
|
| 753 |
+
# GH21158: If inner level json has a key with a null value
|
| 754 |
+
# make sure it does not do a new_d.pop twice and except
|
| 755 |
+
data = {
|
| 756 |
+
"id": None,
|
| 757 |
+
"location": {
|
| 758 |
+
"country": {
|
| 759 |
+
"state": {
|
| 760 |
+
"id": None,
|
| 761 |
+
"town.info": {
|
| 762 |
+
"id": None,
|
| 763 |
+
"region": None,
|
| 764 |
+
"x": 49.151580810546875,
|
| 765 |
+
"y": -33.148521423339844,
|
| 766 |
+
"z": 27.572303771972656,
|
| 767 |
+
},
|
| 768 |
+
}
|
| 769 |
+
}
|
| 770 |
+
},
|
| 771 |
+
}
|
| 772 |
+
result = nested_to_record(data)
|
| 773 |
+
expected = {
|
| 774 |
+
"id": None,
|
| 775 |
+
"location.country.state.id": None,
|
| 776 |
+
"location.country.state.town.info.id": None,
|
| 777 |
+
"location.country.state.town.info.region": None,
|
| 778 |
+
"location.country.state.town.info.x": 49.151580810546875,
|
| 779 |
+
"location.country.state.town.info.y": -33.148521423339844,
|
| 780 |
+
"location.country.state.town.info.z": 27.572303771972656,
|
| 781 |
+
}
|
| 782 |
+
assert result == expected
|
| 783 |
+
|
| 784 |
+
def test_nonetype_multiple_levels(self):
|
| 785 |
+
# GH21158: If inner level json has a key with a null value
|
| 786 |
+
# make sure it does not do a new_d.pop twice and except
|
| 787 |
+
data = {
|
| 788 |
+
"id": None,
|
| 789 |
+
"location": {
|
| 790 |
+
"id": None,
|
| 791 |
+
"country": {
|
| 792 |
+
"id": None,
|
| 793 |
+
"state": {
|
| 794 |
+
"id": None,
|
| 795 |
+
"town.info": {
|
| 796 |
+
"region": None,
|
| 797 |
+
"x": 49.151580810546875,
|
| 798 |
+
"y": -33.148521423339844,
|
| 799 |
+
"z": 27.572303771972656,
|
| 800 |
+
},
|
| 801 |
+
},
|
| 802 |
+
},
|
| 803 |
+
},
|
| 804 |
+
}
|
| 805 |
+
result = nested_to_record(data)
|
| 806 |
+
expected = {
|
| 807 |
+
"id": None,
|
| 808 |
+
"location.id": None,
|
| 809 |
+
"location.country.id": None,
|
| 810 |
+
"location.country.state.id": None,
|
| 811 |
+
"location.country.state.town.info.region": None,
|
| 812 |
+
"location.country.state.town.info.x": 49.151580810546875,
|
| 813 |
+
"location.country.state.town.info.y": -33.148521423339844,
|
| 814 |
+
"location.country.state.town.info.z": 27.572303771972656,
|
| 815 |
+
}
|
| 816 |
+
assert result == expected
|
| 817 |
+
|
| 818 |
+
@pytest.mark.parametrize(
|
| 819 |
+
"max_level, expected",
|
| 820 |
+
[
|
| 821 |
+
(
|
| 822 |
+
None,
|
| 823 |
+
[
|
| 824 |
+
{
|
| 825 |
+
"CreatedBy.Name": "User001",
|
| 826 |
+
"Lookup.TextField": "Some text",
|
| 827 |
+
"Lookup.UserField.Id": "ID001",
|
| 828 |
+
"Lookup.UserField.Name": "Name001",
|
| 829 |
+
"Image.a": "b",
|
| 830 |
+
}
|
| 831 |
+
],
|
| 832 |
+
),
|
| 833 |
+
(
|
| 834 |
+
0,
|
| 835 |
+
[
|
| 836 |
+
{
|
| 837 |
+
"CreatedBy": {"Name": "User001"},
|
| 838 |
+
"Lookup": {
|
| 839 |
+
"TextField": "Some text",
|
| 840 |
+
"UserField": {"Id": "ID001", "Name": "Name001"},
|
| 841 |
+
},
|
| 842 |
+
"Image": {"a": "b"},
|
| 843 |
+
}
|
| 844 |
+
],
|
| 845 |
+
),
|
| 846 |
+
(
|
| 847 |
+
1,
|
| 848 |
+
[
|
| 849 |
+
{
|
| 850 |
+
"CreatedBy.Name": "User001",
|
| 851 |
+
"Lookup.TextField": "Some text",
|
| 852 |
+
"Lookup.UserField": {"Id": "ID001", "Name": "Name001"},
|
| 853 |
+
"Image.a": "b",
|
| 854 |
+
}
|
| 855 |
+
],
|
| 856 |
+
),
|
| 857 |
+
],
|
| 858 |
+
)
|
| 859 |
+
def test_with_max_level(self, max_level, expected):
|
| 860 |
+
# GH23843: Enhanced JSON normalize
|
| 861 |
+
max_level_test_input_data = [
|
| 862 |
+
{
|
| 863 |
+
"CreatedBy": {"Name": "User001"},
|
| 864 |
+
"Lookup": {
|
| 865 |
+
"TextField": "Some text",
|
| 866 |
+
"UserField": {"Id": "ID001", "Name": "Name001"},
|
| 867 |
+
},
|
| 868 |
+
"Image": {"a": "b"},
|
| 869 |
+
}
|
| 870 |
+
]
|
| 871 |
+
output = nested_to_record(max_level_test_input_data, max_level=max_level)
|
| 872 |
+
assert output == expected
|
| 873 |
+
|
| 874 |
+
def test_with_large_max_level(self):
|
| 875 |
+
# GH23843: Enhanced JSON normalize
|
| 876 |
+
max_level = 100
|
| 877 |
+
input_data = [
|
| 878 |
+
{
|
| 879 |
+
"CreatedBy": {
|
| 880 |
+
"user": {
|
| 881 |
+
"name": {"firstname": "Leo", "LastName": "Thomson"},
|
| 882 |
+
"family_tree": {
|
| 883 |
+
"father": {
|
| 884 |
+
"name": "Father001",
|
| 885 |
+
"father": {
|
| 886 |
+
"Name": "Father002",
|
| 887 |
+
"father": {
|
| 888 |
+
"name": "Father003",
|
| 889 |
+
"father": {"Name": "Father004"},
|
| 890 |
+
},
|
| 891 |
+
},
|
| 892 |
+
}
|
| 893 |
+
},
|
| 894 |
+
}
|
| 895 |
+
}
|
| 896 |
+
}
|
| 897 |
+
]
|
| 898 |
+
expected = [
|
| 899 |
+
{
|
| 900 |
+
"CreatedBy.user.name.firstname": "Leo",
|
| 901 |
+
"CreatedBy.user.name.LastName": "Thomson",
|
| 902 |
+
"CreatedBy.user.family_tree.father.name": "Father001",
|
| 903 |
+
"CreatedBy.user.family_tree.father.father.Name": "Father002",
|
| 904 |
+
"CreatedBy.user.family_tree.father.father.father.name": "Father003",
|
| 905 |
+
"CreatedBy.user.family_tree.father.father.father.father.Name": "Father004", # noqa: E501
|
| 906 |
+
}
|
| 907 |
+
]
|
| 908 |
+
output = nested_to_record(input_data, max_level=max_level)
|
| 909 |
+
assert output == expected
|
| 910 |
+
|
| 911 |
+
def test_series_non_zero_index(self):
|
| 912 |
+
# GH 19020
|
| 913 |
+
data = {
|
| 914 |
+
0: {"id": 1, "name": "Foo", "elements": {"a": 1}},
|
| 915 |
+
1: {"id": 2, "name": "Bar", "elements": {"b": 2}},
|
| 916 |
+
2: {"id": 3, "name": "Baz", "elements": {"c": 3}},
|
| 917 |
+
}
|
| 918 |
+
s = Series(data)
|
| 919 |
+
s.index = [1, 2, 3]
|
| 920 |
+
result = json_normalize(s)
|
| 921 |
+
expected = DataFrame(
|
| 922 |
+
{
|
| 923 |
+
"id": [1, 2, 3],
|
| 924 |
+
"name": ["Foo", "Bar", "Baz"],
|
| 925 |
+
"elements.a": [1.0, np.nan, np.nan],
|
| 926 |
+
"elements.b": [np.nan, 2.0, np.nan],
|
| 927 |
+
"elements.c": [np.nan, np.nan, 3.0],
|
| 928 |
+
},
|
| 929 |
+
index=[1, 2, 3],
|
| 930 |
+
)
|
| 931 |
+
tm.assert_frame_equal(result, expected)
|
| 932 |
+
|
| 933 |
+
def test_json_normalize_meta_string_validation(self):
|
| 934 |
+
# GH 63019
|
| 935 |
+
data = [{"a": 1, 12: "meta_value", "nested": [{"b": 2}]}]
|
| 936 |
+
|
| 937 |
+
# Test non-string meta raises TypeError consistently
|
| 938 |
+
with pytest.raises(TypeError, match="must be strings"):
|
| 939 |
+
json_normalize(data, meta=[12])
|
| 940 |
+
|
| 941 |
+
with pytest.raises(TypeError, match="must be strings"):
|
| 942 |
+
json_normalize(data, record_path=["nested"], meta=[12])
|
.venv/lib/python3.13/site-packages/pandas/tests/io/json/test_pandas.py
ADDED
|
@@ -0,0 +1,2387 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import datetime
|
| 2 |
+
from datetime import timedelta
|
| 3 |
+
from io import (
|
| 4 |
+
BytesIO,
|
| 5 |
+
StringIO,
|
| 6 |
+
)
|
| 7 |
+
import json
|
| 8 |
+
import os
|
| 9 |
+
import sys
|
| 10 |
+
import uuid
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
import pytest
|
| 14 |
+
|
| 15 |
+
from pandas._config import using_string_dtype
|
| 16 |
+
|
| 17 |
+
from pandas.compat import IS64
|
| 18 |
+
from pandas.errors import Pandas4Warning
|
| 19 |
+
import pandas.util._test_decorators as td
|
| 20 |
+
|
| 21 |
+
import pandas as pd
|
| 22 |
+
from pandas import (
|
| 23 |
+
NA,
|
| 24 |
+
DataFrame,
|
| 25 |
+
DatetimeIndex,
|
| 26 |
+
Index,
|
| 27 |
+
RangeIndex,
|
| 28 |
+
Series,
|
| 29 |
+
Timestamp,
|
| 30 |
+
date_range,
|
| 31 |
+
read_json,
|
| 32 |
+
)
|
| 33 |
+
import pandas._testing as tm
|
| 34 |
+
|
| 35 |
+
from pandas.io.json import ujson_dumps
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def test_literal_json_raises():
|
| 39 |
+
# PR 53409
|
| 40 |
+
jsonl = """{"a": 1, "b": 2}
|
| 41 |
+
{"a": 3, "b": 4}
|
| 42 |
+
{"a": 5, "b": 6}
|
| 43 |
+
{"a": 7, "b": 8}"""
|
| 44 |
+
|
| 45 |
+
msg = r".* does not exist"
|
| 46 |
+
|
| 47 |
+
with pytest.raises(FileNotFoundError, match=msg):
|
| 48 |
+
read_json(jsonl, lines=False)
|
| 49 |
+
|
| 50 |
+
with pytest.raises(FileNotFoundError, match=msg):
|
| 51 |
+
read_json('{"a": 1, "b": 2}\n{"b":2, "a" :1}\n', lines=True)
|
| 52 |
+
|
| 53 |
+
with pytest.raises(FileNotFoundError, match=msg):
|
| 54 |
+
read_json(
|
| 55 |
+
'{"a\\\\":"foo\\\\","b":"bar"}\n{"a\\\\":"foo\\"","b":"bar"}\n',
|
| 56 |
+
lines=False,
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
with pytest.raises(FileNotFoundError, match=msg):
|
| 60 |
+
read_json('{"a": 1, "b": 2}\n{"b":2, "a" :1}\n', lines=False)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def assert_json_roundtrip_equal(result, expected, orient):
|
| 64 |
+
if orient in ("records", "values"):
|
| 65 |
+
expected = expected.reset_index(drop=True)
|
| 66 |
+
if orient == "values":
|
| 67 |
+
expected.columns = range(len(expected.columns))
|
| 68 |
+
tm.assert_frame_equal(result, expected)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
class TestPandasContainer:
|
| 72 |
+
@pytest.fixture
|
| 73 |
+
def datetime_series(self):
|
| 74 |
+
# Same as usual datetime_series, but with index freq set to None,
|
| 75 |
+
# since that doesn't round-trip, see GH#33711
|
| 76 |
+
ser = Series(
|
| 77 |
+
1.1 * np.arange(10, dtype=np.float64),
|
| 78 |
+
index=date_range("2020-01-01", periods=10, unit="ns"),
|
| 79 |
+
name="ts",
|
| 80 |
+
)
|
| 81 |
+
ser.index = ser.index._with_freq(None)
|
| 82 |
+
return ser
|
| 83 |
+
|
| 84 |
+
@pytest.fixture
|
| 85 |
+
def datetime_frame(self):
|
| 86 |
+
# Same as usual datetime_frame, but with index freq set to None,
|
| 87 |
+
# since that doesn't round-trip, see GH#33711
|
| 88 |
+
df = DataFrame(
|
| 89 |
+
np.random.default_rng(2).standard_normal((30, 4)),
|
| 90 |
+
columns=Index(list("ABCD")),
|
| 91 |
+
index=date_range("2000-01-01", periods=30, freq="B", unit="ns"),
|
| 92 |
+
)
|
| 93 |
+
df.index = df.index._with_freq(None)
|
| 94 |
+
return df
|
| 95 |
+
|
| 96 |
+
def test_frame_double_encoded_labels(self, orient):
|
| 97 |
+
df = DataFrame(
|
| 98 |
+
[["a", "b"], ["c", "d"]],
|
| 99 |
+
index=['index " 1', "index / 2"],
|
| 100 |
+
columns=["a \\ b", "y / z"],
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
data = StringIO(df.to_json(orient=orient))
|
| 104 |
+
result = read_json(data, orient=orient)
|
| 105 |
+
expected = df.copy()
|
| 106 |
+
assert_json_roundtrip_equal(result, expected, orient)
|
| 107 |
+
|
| 108 |
+
@pytest.mark.parametrize("orient", ["split", "records", "values"])
|
| 109 |
+
def test_frame_non_unique_index(self, orient):
|
| 110 |
+
df = DataFrame([["a", "b"], ["c", "d"]], index=[1, 1], columns=["x", "y"])
|
| 111 |
+
data = StringIO(df.to_json(orient=orient))
|
| 112 |
+
result = read_json(data, orient=orient)
|
| 113 |
+
expected = df.copy()
|
| 114 |
+
|
| 115 |
+
assert_json_roundtrip_equal(result, expected, orient)
|
| 116 |
+
|
| 117 |
+
@pytest.mark.parametrize("orient", ["index", "columns"])
|
| 118 |
+
def test_frame_non_unique_index_raises(self, orient):
|
| 119 |
+
df = DataFrame([["a", "b"], ["c", "d"]], index=[1, 1], columns=["x", "y"])
|
| 120 |
+
msg = f"DataFrame index must be unique for orient='{orient}'"
|
| 121 |
+
with pytest.raises(ValueError, match=msg):
|
| 122 |
+
df.to_json(orient=orient)
|
| 123 |
+
|
| 124 |
+
@pytest.mark.parametrize("orient", ["split", "values"])
|
| 125 |
+
@pytest.mark.parametrize(
|
| 126 |
+
"data",
|
| 127 |
+
[
|
| 128 |
+
[["a", "b"], ["c", "d"]],
|
| 129 |
+
[[1.5, 2.5], [3.5, 4.5]],
|
| 130 |
+
[[1, 2.5], [3, 4.5]],
|
| 131 |
+
[
|
| 132 |
+
[Timestamp("20130101").as_unit("s"), 3.5],
|
| 133 |
+
[Timestamp("20130102").as_unit("s"), 4.5],
|
| 134 |
+
],
|
| 135 |
+
],
|
| 136 |
+
)
|
| 137 |
+
def test_frame_non_unique_columns(self, orient, data, request):
|
| 138 |
+
df = DataFrame(data, index=[1, 2], columns=["x", "x"])
|
| 139 |
+
|
| 140 |
+
expected_warning = None
|
| 141 |
+
msg = (
|
| 142 |
+
"The default 'epoch' date format is deprecated and will be removed "
|
| 143 |
+
"in a future version, please use 'iso' date format instead."
|
| 144 |
+
)
|
| 145 |
+
if df.iloc[:, 0].dtype == "datetime64[s]":
|
| 146 |
+
expected_warning = Pandas4Warning
|
| 147 |
+
|
| 148 |
+
with tm.assert_produces_warning(expected_warning, match=msg):
|
| 149 |
+
result = read_json(
|
| 150 |
+
StringIO(df.to_json(orient=orient)), orient=orient, convert_dates=["x"]
|
| 151 |
+
)
|
| 152 |
+
if orient == "values":
|
| 153 |
+
expected = DataFrame(data)
|
| 154 |
+
if expected.iloc[:, 0].dtype == "datetime64[s]":
|
| 155 |
+
# orient == "values" by default will write Timestamp objects out
|
| 156 |
+
# in milliseconds;
|
| 157 |
+
# TODO: a to_epoch method would also solve; see GH 14772
|
| 158 |
+
dta = expected.iloc[:, 0]._values
|
| 159 |
+
expected.isetitem(0, dta.as_unit("ms").astype(np.int64))
|
| 160 |
+
elif orient == "split":
|
| 161 |
+
expected = df
|
| 162 |
+
expected.columns = ["x", "x.1"]
|
| 163 |
+
if expected["x"].dtype.kind == "M":
|
| 164 |
+
expected["x"] = expected["x"].astype("M8[ms]")
|
| 165 |
+
|
| 166 |
+
tm.assert_frame_equal(result, expected)
|
| 167 |
+
|
| 168 |
+
@pytest.mark.parametrize("orient", ["index", "columns", "records"])
|
| 169 |
+
def test_frame_non_unique_columns_raises(self, orient):
|
| 170 |
+
df = DataFrame([["a", "b"], ["c", "d"]], index=[1, 2], columns=["x", "x"])
|
| 171 |
+
|
| 172 |
+
msg = f"DataFrame columns must be unique for orient='{orient}'"
|
| 173 |
+
with pytest.raises(ValueError, match=msg):
|
| 174 |
+
df.to_json(orient=orient)
|
| 175 |
+
|
| 176 |
+
def test_frame_default_orient(self, float_frame):
|
| 177 |
+
assert float_frame.to_json() == float_frame.to_json(orient="columns")
|
| 178 |
+
|
| 179 |
+
@pytest.mark.parametrize("dtype", [False, float])
|
| 180 |
+
@pytest.mark.parametrize("convert_axes", [True, False])
|
| 181 |
+
def test_roundtrip_simple(self, orient, convert_axes, dtype, float_frame):
|
| 182 |
+
data = StringIO(float_frame.to_json(orient=orient))
|
| 183 |
+
result = read_json(data, orient=orient, convert_axes=convert_axes, dtype=dtype)
|
| 184 |
+
|
| 185 |
+
expected = float_frame
|
| 186 |
+
|
| 187 |
+
assert_json_roundtrip_equal(result, expected, orient)
|
| 188 |
+
|
| 189 |
+
@pytest.mark.parametrize("dtype", [False, np.int64])
|
| 190 |
+
@pytest.mark.parametrize("convert_axes", [True, False])
|
| 191 |
+
def test_roundtrip_intframe(self, orient, convert_axes, dtype, int_frame):
|
| 192 |
+
data = StringIO(int_frame.to_json(orient=orient))
|
| 193 |
+
result = read_json(data, orient=orient, convert_axes=convert_axes, dtype=dtype)
|
| 194 |
+
expected = int_frame
|
| 195 |
+
assert_json_roundtrip_equal(result, expected, orient)
|
| 196 |
+
|
| 197 |
+
@pytest.mark.parametrize("dtype", [None, np.float64, int, "U3"])
|
| 198 |
+
@pytest.mark.parametrize("convert_axes", [True, False])
|
| 199 |
+
def test_roundtrip_str_axes(self, orient, convert_axes, dtype):
|
| 200 |
+
df = DataFrame(
|
| 201 |
+
np.zeros((200, 4)),
|
| 202 |
+
columns=[str(i) for i in range(4)],
|
| 203 |
+
index=[str(i) for i in range(200)],
|
| 204 |
+
dtype=dtype,
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
data = StringIO(df.to_json(orient=orient))
|
| 208 |
+
result = read_json(data, orient=orient, convert_axes=convert_axes, dtype=dtype)
|
| 209 |
+
|
| 210 |
+
expected = df.copy()
|
| 211 |
+
if not dtype:
|
| 212 |
+
expected = expected.astype(np.int64)
|
| 213 |
+
|
| 214 |
+
# index columns, and records orients cannot fully preserve the string
|
| 215 |
+
# dtype for axes as the index and column labels are used as keys in
|
| 216 |
+
# JSON objects. JSON keys are by definition strings, so there's no way
|
| 217 |
+
# to disambiguate whether those keys actually were strings or numeric
|
| 218 |
+
# beforehand and numeric wins out.
|
| 219 |
+
if convert_axes and (orient in ("index", "columns")):
|
| 220 |
+
expected.columns = expected.columns.astype(np.int64)
|
| 221 |
+
expected.index = expected.index.astype(np.int64)
|
| 222 |
+
elif orient == "records" and convert_axes:
|
| 223 |
+
expected.columns = expected.columns.astype(np.int64)
|
| 224 |
+
elif convert_axes and orient == "split":
|
| 225 |
+
expected.columns = expected.columns.astype(np.int64)
|
| 226 |
+
|
| 227 |
+
assert_json_roundtrip_equal(result, expected, orient)
|
| 228 |
+
|
| 229 |
+
@pytest.mark.parametrize("convert_axes", [True, False])
|
| 230 |
+
def test_roundtrip_categorical(
|
| 231 |
+
self, request, orient, convert_axes, using_infer_string
|
| 232 |
+
):
|
| 233 |
+
# TODO: create a better frame to test with and improve coverage
|
| 234 |
+
if orient in ("index", "columns"):
|
| 235 |
+
request.applymarker(
|
| 236 |
+
pytest.mark.xfail(
|
| 237 |
+
reason=f"Can't have duplicate index values for orient '{orient}')"
|
| 238 |
+
)
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
data = {
|
| 242 |
+
c: np.random.default_rng(i).standard_normal(30)
|
| 243 |
+
for i, c in enumerate(list("ABCD"))
|
| 244 |
+
}
|
| 245 |
+
cat = ["bah"] * 5 + ["bar"] * 5 + ["baz"] * 5 + ["foo"] * 15
|
| 246 |
+
data["E"] = list(reversed(cat))
|
| 247 |
+
data["sort"] = np.arange(30, dtype="int64")
|
| 248 |
+
categorical_frame = DataFrame(data, index=pd.CategoricalIndex(cat, name="E"))
|
| 249 |
+
data = StringIO(categorical_frame.to_json(orient=orient))
|
| 250 |
+
result = read_json(data, orient=orient, convert_axes=convert_axes)
|
| 251 |
+
|
| 252 |
+
expected = categorical_frame.copy()
|
| 253 |
+
expected.index = expected.index.astype(
|
| 254 |
+
str if not using_infer_string else "str"
|
| 255 |
+
) # Categorical not preserved
|
| 256 |
+
expected.index.name = None # index names aren't preserved in JSON
|
| 257 |
+
assert_json_roundtrip_equal(result, expected, orient)
|
| 258 |
+
|
| 259 |
+
@pytest.mark.parametrize("convert_axes", [True, False])
|
| 260 |
+
def test_roundtrip_empty(self, orient, convert_axes):
|
| 261 |
+
empty_frame = DataFrame()
|
| 262 |
+
data = StringIO(empty_frame.to_json(orient=orient))
|
| 263 |
+
result = read_json(data, orient=orient, convert_axes=convert_axes)
|
| 264 |
+
if orient == "split":
|
| 265 |
+
idx = Index([], dtype=(float if convert_axes else object))
|
| 266 |
+
expected = DataFrame(index=idx, columns=idx)
|
| 267 |
+
elif orient in ["index", "columns"]:
|
| 268 |
+
expected = DataFrame()
|
| 269 |
+
else:
|
| 270 |
+
expected = empty_frame.copy()
|
| 271 |
+
|
| 272 |
+
tm.assert_frame_equal(result, expected)
|
| 273 |
+
|
| 274 |
+
@pytest.mark.parametrize(
|
| 275 |
+
"dtype_backend", [None, pytest.param("pyarrow", marks=td.skip_if_no("pyarrow"))]
|
| 276 |
+
)
|
| 277 |
+
@pytest.mark.parametrize("convert_axes", [True, False])
|
| 278 |
+
def test_roundtrip_timestamp(
|
| 279 |
+
self, orient, convert_axes, dtype_backend, datetime_frame
|
| 280 |
+
):
|
| 281 |
+
# TODO: improve coverage with date_format parameter
|
| 282 |
+
expected = datetime_frame.copy()
|
| 283 |
+
if dtype_backend is not None:
|
| 284 |
+
datetime_frame.index = Series(datetime_frame.index).convert_dtypes(
|
| 285 |
+
dtype_backend=dtype_backend
|
| 286 |
+
)
|
| 287 |
+
data = StringIO(datetime_frame.to_json(orient=orient))
|
| 288 |
+
result = read_json(data, orient=orient, convert_axes=convert_axes)
|
| 289 |
+
|
| 290 |
+
if not convert_axes: # one off for ts handling
|
| 291 |
+
# DTI gets converted to epoch values
|
| 292 |
+
idx = expected.index.view(np.int64) // 1000000
|
| 293 |
+
if orient != "split": # TODO: handle consistently across orients
|
| 294 |
+
idx = idx.astype(str)
|
| 295 |
+
|
| 296 |
+
expected.index = idx
|
| 297 |
+
else:
|
| 298 |
+
expected.index = expected.index.as_unit("ms")
|
| 299 |
+
|
| 300 |
+
assert_json_roundtrip_equal(result, expected, orient)
|
| 301 |
+
|
| 302 |
+
@pytest.mark.parametrize("convert_axes", [True, False])
|
| 303 |
+
def test_roundtrip_mixed(self, orient, convert_axes):
|
| 304 |
+
index = Index(["a", "b", "c", "d", "e"])
|
| 305 |
+
values = {
|
| 306 |
+
"A": [0.0, 1.0, 2.0, 3.0, 4.0],
|
| 307 |
+
"B": [0.0, 1.0, 0.0, 1.0, 0.0],
|
| 308 |
+
"C": ["foo1", "foo2", "foo3", "foo4", "foo5"],
|
| 309 |
+
"D": [True, False, True, False, True],
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
df = DataFrame(data=values, index=index)
|
| 313 |
+
|
| 314 |
+
data = StringIO(df.to_json(orient=orient))
|
| 315 |
+
result = read_json(data, orient=orient, convert_axes=convert_axes)
|
| 316 |
+
|
| 317 |
+
expected = df.copy()
|
| 318 |
+
expected = expected.assign(**expected.select_dtypes("number").astype(np.int64))
|
| 319 |
+
|
| 320 |
+
assert_json_roundtrip_equal(result, expected, orient)
|
| 321 |
+
|
| 322 |
+
@pytest.mark.xfail(
|
| 323 |
+
reason="#50456 Column multiindex is stored and loaded differently",
|
| 324 |
+
raises=AssertionError,
|
| 325 |
+
)
|
| 326 |
+
@pytest.mark.parametrize(
|
| 327 |
+
"columns",
|
| 328 |
+
[
|
| 329 |
+
[["2022", "2022"], ["JAN", "FEB"]],
|
| 330 |
+
[["2022", "2023"], ["JAN", "JAN"]],
|
| 331 |
+
[["2022", "2022"], ["JAN", "JAN"]],
|
| 332 |
+
],
|
| 333 |
+
)
|
| 334 |
+
def test_roundtrip_multiindex(self, columns):
|
| 335 |
+
df = DataFrame(
|
| 336 |
+
[[1, 2], [3, 4]],
|
| 337 |
+
columns=pd.MultiIndex.from_arrays(columns),
|
| 338 |
+
)
|
| 339 |
+
data = StringIO(df.to_json(orient="split"))
|
| 340 |
+
result = read_json(data, orient="split")
|
| 341 |
+
tm.assert_frame_equal(result, df)
|
| 342 |
+
|
| 343 |
+
@pytest.mark.parametrize(
|
| 344 |
+
"data,msg,orient",
|
| 345 |
+
[
|
| 346 |
+
('{"key":b:a:d}', "Expected object or value", "columns"),
|
| 347 |
+
# too few indices
|
| 348 |
+
(
|
| 349 |
+
'{"columns":["A","B"],'
|
| 350 |
+
'"index":["2","3"],'
|
| 351 |
+
'"data":[[1.0,"1"],[2.0,"2"],[null,"3"]]}',
|
| 352 |
+
"|".join(
|
| 353 |
+
[
|
| 354 |
+
r"Length of values \(3\) does not match length of index \(2\)",
|
| 355 |
+
]
|
| 356 |
+
),
|
| 357 |
+
"split",
|
| 358 |
+
),
|
| 359 |
+
# too many columns
|
| 360 |
+
(
|
| 361 |
+
'{"columns":["A","B","C"],'
|
| 362 |
+
'"index":["1","2","3"],'
|
| 363 |
+
'"data":[[1.0,"1"],[2.0,"2"],[null,"3"]]}',
|
| 364 |
+
"3 columns passed, passed data had 2 columns",
|
| 365 |
+
"split",
|
| 366 |
+
),
|
| 367 |
+
# bad key
|
| 368 |
+
(
|
| 369 |
+
'{"badkey":["A","B"],'
|
| 370 |
+
'"index":["2","3"],'
|
| 371 |
+
'"data":[[1.0,"1"],[2.0,"2"],[null,"3"]]}',
|
| 372 |
+
r"unexpected key\(s\): badkey",
|
| 373 |
+
"split",
|
| 374 |
+
),
|
| 375 |
+
],
|
| 376 |
+
)
|
| 377 |
+
def test_frame_from_json_bad_data_raises(self, data, msg, orient):
|
| 378 |
+
with pytest.raises(ValueError, match=msg):
|
| 379 |
+
read_json(StringIO(data), orient=orient)
|
| 380 |
+
|
| 381 |
+
@pytest.mark.parametrize("dtype", [True, False])
|
| 382 |
+
@pytest.mark.parametrize("convert_axes", [True, False])
|
| 383 |
+
def test_frame_from_json_missing_data(self, orient, convert_axes, dtype):
|
| 384 |
+
num_df = DataFrame([[1, 2], [4, 5, 6]])
|
| 385 |
+
|
| 386 |
+
result = read_json(
|
| 387 |
+
StringIO(num_df.to_json(orient=orient)),
|
| 388 |
+
orient=orient,
|
| 389 |
+
convert_axes=convert_axes,
|
| 390 |
+
dtype=dtype,
|
| 391 |
+
)
|
| 392 |
+
assert np.isnan(result.iloc[0, 2])
|
| 393 |
+
|
| 394 |
+
obj_df = DataFrame([["1", "2"], ["4", "5", "6"]])
|
| 395 |
+
result = read_json(
|
| 396 |
+
StringIO(obj_df.to_json(orient=orient)),
|
| 397 |
+
orient=orient,
|
| 398 |
+
convert_axes=convert_axes,
|
| 399 |
+
dtype=dtype,
|
| 400 |
+
)
|
| 401 |
+
assert np.isnan(result.iloc[0, 2])
|
| 402 |
+
|
| 403 |
+
@pytest.mark.parametrize("dtype", [True, False])
|
| 404 |
+
def test_frame_read_json_dtype_missing_value(self, dtype):
|
| 405 |
+
# GH28501 Parse missing values using read_json with dtype=False
|
| 406 |
+
# to NaN instead of None
|
| 407 |
+
result = read_json(StringIO("[null]"), dtype=dtype)
|
| 408 |
+
expected = DataFrame([np.nan], dtype=object if not dtype else None)
|
| 409 |
+
|
| 410 |
+
tm.assert_frame_equal(result, expected)
|
| 411 |
+
|
| 412 |
+
@pytest.mark.parametrize("inf", [np.inf, -np.inf])
|
| 413 |
+
@pytest.mark.parametrize("dtype", [True, False])
|
| 414 |
+
def test_frame_infinity(self, inf, dtype):
|
| 415 |
+
# infinities get mapped to nulls which get mapped to NaNs during
|
| 416 |
+
# deserialisation
|
| 417 |
+
df = DataFrame([[1, 2], [4, 5, 6]])
|
| 418 |
+
df.loc[0, 2] = inf
|
| 419 |
+
|
| 420 |
+
data = StringIO(df.to_json())
|
| 421 |
+
result = read_json(data, dtype=dtype)
|
| 422 |
+
assert np.isnan(result.iloc[0, 2])
|
| 423 |
+
|
| 424 |
+
@pytest.mark.skipif(not IS64, reason="not compliant on 32-bit, xref #15865")
|
| 425 |
+
@pytest.mark.parametrize(
|
| 426 |
+
"value,precision,expected_val",
|
| 427 |
+
[
|
| 428 |
+
(0.95, 1, 1.0),
|
| 429 |
+
(1.95, 1, 2.0),
|
| 430 |
+
(-1.95, 1, -2.0),
|
| 431 |
+
(0.995, 2, 1.0),
|
| 432 |
+
(0.9995, 3, 1.0),
|
| 433 |
+
(0.99999999999999944, 15, 1.0),
|
| 434 |
+
],
|
| 435 |
+
)
|
| 436 |
+
def test_frame_to_json_float_precision(self, value, precision, expected_val):
|
| 437 |
+
df = DataFrame([{"a_float": value}])
|
| 438 |
+
encoded = df.to_json(double_precision=precision)
|
| 439 |
+
assert encoded == f'{{"a_float":{{"0":{expected_val}}}}}'
|
| 440 |
+
|
| 441 |
+
def test_frame_to_json_except(self):
|
| 442 |
+
df = DataFrame([1, 2, 3])
|
| 443 |
+
msg = "Invalid value 'garbage' for option 'orient'"
|
| 444 |
+
with pytest.raises(ValueError, match=msg):
|
| 445 |
+
df.to_json(orient="garbage")
|
| 446 |
+
|
| 447 |
+
def test_frame_empty(self):
|
| 448 |
+
df = DataFrame(columns=["jim", "joe"])
|
| 449 |
+
assert not df._is_mixed_type
|
| 450 |
+
|
| 451 |
+
data = StringIO(df.to_json())
|
| 452 |
+
result = read_json(data, dtype=dict(df.dtypes))
|
| 453 |
+
tm.assert_frame_equal(result, df, check_index_type=False)
|
| 454 |
+
|
| 455 |
+
def test_frame_empty_to_json(self):
|
| 456 |
+
# GH 7445
|
| 457 |
+
df = DataFrame({"test": []}, index=[])
|
| 458 |
+
result = df.to_json(orient="columns")
|
| 459 |
+
expected = '{"test":{}}'
|
| 460 |
+
assert result == expected
|
| 461 |
+
|
| 462 |
+
def test_frame_empty_mixedtype(self):
|
| 463 |
+
# mixed type
|
| 464 |
+
df = DataFrame(columns=["jim", "joe"])
|
| 465 |
+
df["joe"] = df["joe"].astype("i8")
|
| 466 |
+
assert df._is_mixed_type
|
| 467 |
+
data = df.to_json()
|
| 468 |
+
tm.assert_frame_equal(
|
| 469 |
+
read_json(StringIO(data), dtype=dict(df.dtypes)),
|
| 470 |
+
df,
|
| 471 |
+
check_index_type=False,
|
| 472 |
+
)
|
| 473 |
+
|
| 474 |
+
def test_frame_mixedtype_orient(self): # GH10289
|
| 475 |
+
vals = [
|
| 476 |
+
[10, 1, "foo", 0.1, 0.01],
|
| 477 |
+
[20, 2, "bar", 0.2, 0.02],
|
| 478 |
+
[30, 3, "baz", 0.3, 0.03],
|
| 479 |
+
[40, 4, "qux", 0.4, 0.04],
|
| 480 |
+
]
|
| 481 |
+
|
| 482 |
+
df = DataFrame(
|
| 483 |
+
vals, index=list("abcd"), columns=["1st", "2nd", "3rd", "4th", "5th"]
|
| 484 |
+
)
|
| 485 |
+
|
| 486 |
+
assert df._is_mixed_type
|
| 487 |
+
right = df.copy()
|
| 488 |
+
|
| 489 |
+
for orient in ["split", "index", "columns"]:
|
| 490 |
+
inp = StringIO(df.to_json(orient=orient))
|
| 491 |
+
left = read_json(inp, orient=orient, convert_axes=False)
|
| 492 |
+
tm.assert_frame_equal(left, right)
|
| 493 |
+
|
| 494 |
+
right.index = RangeIndex(len(df))
|
| 495 |
+
inp = StringIO(df.to_json(orient="records"))
|
| 496 |
+
left = read_json(inp, orient="records", convert_axes=False)
|
| 497 |
+
tm.assert_frame_equal(left, right)
|
| 498 |
+
|
| 499 |
+
right.columns = RangeIndex(df.shape[1])
|
| 500 |
+
inp = StringIO(df.to_json(orient="values"))
|
| 501 |
+
left = read_json(inp, orient="values", convert_axes=False)
|
| 502 |
+
tm.assert_frame_equal(left, right)
|
| 503 |
+
|
| 504 |
+
def test_v12_compat(self, datapath):
|
| 505 |
+
dti = date_range("2000-01-03", "2000-01-07", unit="ns")
|
| 506 |
+
# freq doesn't roundtrip
|
| 507 |
+
dti = DatetimeIndex(np.asarray(dti), freq=None)
|
| 508 |
+
df = DataFrame(
|
| 509 |
+
[
|
| 510 |
+
[1.56808523, 0.65727391, 1.81021139, -0.17251653],
|
| 511 |
+
[-0.2550111, -0.08072427, -0.03202878, -0.17581665],
|
| 512 |
+
[1.51493992, 0.11805825, 1.629455, -1.31506612],
|
| 513 |
+
[-0.02765498, 0.44679743, 0.33192641, -0.27885413],
|
| 514 |
+
[0.05951614, -2.69652057, 1.28163262, 0.34703478],
|
| 515 |
+
],
|
| 516 |
+
columns=["A", "B", "C", "D"],
|
| 517 |
+
index=dti,
|
| 518 |
+
)
|
| 519 |
+
df["date"] = Timestamp("19920106 18:21:32.12").as_unit("ns")
|
| 520 |
+
df.iloc[3, df.columns.get_loc("date")] = Timestamp("20130101")
|
| 521 |
+
df["modified"] = df["date"]
|
| 522 |
+
df.iloc[1, df.columns.get_loc("modified")] = pd.NaT
|
| 523 |
+
|
| 524 |
+
dirpath = datapath("io", "json", "data")
|
| 525 |
+
v12_json = os.path.join(dirpath, "tsframe_v012.json")
|
| 526 |
+
result = read_json(v12_json)
|
| 527 |
+
expected = df.copy()
|
| 528 |
+
expected["date"] = expected["date"].dt.as_unit("ns")
|
| 529 |
+
expected["modified"] = expected["modified"].dt.as_unit("ns")
|
| 530 |
+
expected.index = expected.index.as_unit("ns")
|
| 531 |
+
tm.assert_frame_equal(result, expected)
|
| 532 |
+
|
| 533 |
+
v12_iso_json = os.path.join(dirpath, "tsframe_iso_v012.json")
|
| 534 |
+
result_iso = read_json(v12_iso_json)
|
| 535 |
+
expected_iso = df.drop(["modified"], axis=1)
|
| 536 |
+
expected_iso["date"] = expected_iso["date"].dt.as_unit("us")
|
| 537 |
+
expected_iso.index = expected_iso.index.as_unit("us")
|
| 538 |
+
tm.assert_frame_equal(result_iso, expected_iso, check_column_type=False)
|
| 539 |
+
|
| 540 |
+
def test_blocks_compat_GH9037(self, using_infer_string):
|
| 541 |
+
index = date_range("20000101", periods=10, freq="h", unit="ns")
|
| 542 |
+
# freq doesn't round-trip
|
| 543 |
+
index = DatetimeIndex(list(index), freq=None)
|
| 544 |
+
|
| 545 |
+
df_mixed = DataFrame(
|
| 546 |
+
{
|
| 547 |
+
"float_1": [
|
| 548 |
+
-0.92077639,
|
| 549 |
+
0.77434435,
|
| 550 |
+
1.25234727,
|
| 551 |
+
0.61485564,
|
| 552 |
+
-0.60316077,
|
| 553 |
+
0.24653374,
|
| 554 |
+
0.28668979,
|
| 555 |
+
-2.51969012,
|
| 556 |
+
0.95748401,
|
| 557 |
+
-1.02970536,
|
| 558 |
+
],
|
| 559 |
+
"int_1": [
|
| 560 |
+
19680418,
|
| 561 |
+
75337055,
|
| 562 |
+
99973684,
|
| 563 |
+
65103179,
|
| 564 |
+
79373900,
|
| 565 |
+
40314334,
|
| 566 |
+
21290235,
|
| 567 |
+
4991321,
|
| 568 |
+
41903419,
|
| 569 |
+
16008365,
|
| 570 |
+
],
|
| 571 |
+
"str_1": [
|
| 572 |
+
"78c608f1",
|
| 573 |
+
"64a99743",
|
| 574 |
+
"13d2ff52",
|
| 575 |
+
"ca7f4af2",
|
| 576 |
+
"97236474",
|
| 577 |
+
"bde7e214",
|
| 578 |
+
"1a6bde47",
|
| 579 |
+
"b1190be5",
|
| 580 |
+
"7a669144",
|
| 581 |
+
"8d64d068",
|
| 582 |
+
],
|
| 583 |
+
"float_2": [
|
| 584 |
+
-0.0428278,
|
| 585 |
+
-1.80872357,
|
| 586 |
+
3.36042349,
|
| 587 |
+
-0.7573685,
|
| 588 |
+
-0.48217572,
|
| 589 |
+
0.86229683,
|
| 590 |
+
1.08935819,
|
| 591 |
+
0.93898739,
|
| 592 |
+
-0.03030452,
|
| 593 |
+
1.43366348,
|
| 594 |
+
],
|
| 595 |
+
"str_2": [
|
| 596 |
+
"14f04af9",
|
| 597 |
+
"d085da90",
|
| 598 |
+
"4bcfac83",
|
| 599 |
+
"81504caf",
|
| 600 |
+
"2ffef4a9",
|
| 601 |
+
"08e2f5c4",
|
| 602 |
+
"07e1af03",
|
| 603 |
+
"addbd4a7",
|
| 604 |
+
"1f6a09ba",
|
| 605 |
+
"4bfc4d87",
|
| 606 |
+
],
|
| 607 |
+
"int_2": [
|
| 608 |
+
86967717,
|
| 609 |
+
98098830,
|
| 610 |
+
51927505,
|
| 611 |
+
20372254,
|
| 612 |
+
12601730,
|
| 613 |
+
20884027,
|
| 614 |
+
34193846,
|
| 615 |
+
10561746,
|
| 616 |
+
24867120,
|
| 617 |
+
76131025,
|
| 618 |
+
],
|
| 619 |
+
},
|
| 620 |
+
index=index,
|
| 621 |
+
)
|
| 622 |
+
|
| 623 |
+
# JSON deserialisation always creates unicode strings
|
| 624 |
+
df_mixed.columns = df_mixed.columns.astype(
|
| 625 |
+
np.str_ if not using_infer_string else "str"
|
| 626 |
+
)
|
| 627 |
+
data = StringIO(df_mixed.to_json(orient="split"))
|
| 628 |
+
df_roundtrip = read_json(data, orient="split")
|
| 629 |
+
df_mixed.index = df_mixed.index.as_unit("ms")
|
| 630 |
+
tm.assert_frame_equal(
|
| 631 |
+
df_mixed,
|
| 632 |
+
df_roundtrip,
|
| 633 |
+
check_index_type=True,
|
| 634 |
+
check_column_type=True,
|
| 635 |
+
by_blocks=True,
|
| 636 |
+
check_exact=True,
|
| 637 |
+
)
|
| 638 |
+
|
| 639 |
+
def test_frame_nonprintable_bytes(self):
|
| 640 |
+
# GH14256: failing column caused segfaults, if it is not the last one
|
| 641 |
+
|
| 642 |
+
class BinaryThing:
|
| 643 |
+
def __init__(self, hexed) -> None:
|
| 644 |
+
self.hexed = hexed
|
| 645 |
+
self.binary = bytes.fromhex(hexed)
|
| 646 |
+
|
| 647 |
+
def __str__(self) -> str:
|
| 648 |
+
return self.hexed
|
| 649 |
+
|
| 650 |
+
hexed = "574b4454ba8c5eb4f98a8f45"
|
| 651 |
+
binthing = BinaryThing(hexed)
|
| 652 |
+
|
| 653 |
+
# verify the proper conversion of printable content
|
| 654 |
+
df_printable = DataFrame({"A": [binthing.hexed]})
|
| 655 |
+
assert df_printable.to_json() == f'{{"A":{{"0":"{hexed}"}}}}'
|
| 656 |
+
|
| 657 |
+
# check if non-printable content throws appropriate Exception
|
| 658 |
+
df_nonprintable = DataFrame({"A": [binthing]})
|
| 659 |
+
msg = "Unsupported UTF-8 sequence length when encoding string"
|
| 660 |
+
with pytest.raises(OverflowError, match=msg):
|
| 661 |
+
df_nonprintable.to_json()
|
| 662 |
+
|
| 663 |
+
# the same with multiple columns threw segfaults
|
| 664 |
+
df_mixed = DataFrame({"A": [binthing], "B": [1]}, columns=["A", "B"])
|
| 665 |
+
with pytest.raises(OverflowError, match=msg):
|
| 666 |
+
df_mixed.to_json()
|
| 667 |
+
|
| 668 |
+
# default_handler should resolve exceptions for non-string types
|
| 669 |
+
result = df_nonprintable.to_json(default_handler=str)
|
| 670 |
+
expected = f'{{"A":{{"0":"{hexed}"}}}}'
|
| 671 |
+
assert result == expected
|
| 672 |
+
assert (
|
| 673 |
+
df_mixed.to_json(default_handler=str)
|
| 674 |
+
== f'{{"A":{{"0":"{hexed}"}},"B":{{"0":1}}}}'
|
| 675 |
+
)
|
| 676 |
+
|
| 677 |
+
def test_label_overflow(self):
|
| 678 |
+
# GH14256: buffer length not checked when writing label
|
| 679 |
+
result = DataFrame({"bar" * 100000: [1], "foo": [1337]}).to_json()
|
| 680 |
+
expected = f'{{"{"bar" * 100000}":{{"0":1}},"foo":{{"0":1337}}}}'
|
| 681 |
+
assert result == expected
|
| 682 |
+
|
| 683 |
+
def test_series_non_unique_index(self):
|
| 684 |
+
s = Series(["a", "b"], index=[1, 1])
|
| 685 |
+
|
| 686 |
+
msg = "Series index must be unique for orient='index'"
|
| 687 |
+
with pytest.raises(ValueError, match=msg):
|
| 688 |
+
s.to_json(orient="index")
|
| 689 |
+
|
| 690 |
+
tm.assert_series_equal(
|
| 691 |
+
s,
|
| 692 |
+
read_json(
|
| 693 |
+
StringIO(s.to_json(orient="split")), orient="split", typ="series"
|
| 694 |
+
),
|
| 695 |
+
)
|
| 696 |
+
unserialized = read_json(
|
| 697 |
+
StringIO(s.to_json(orient="records")), orient="records", typ="series"
|
| 698 |
+
)
|
| 699 |
+
tm.assert_equal(s.values, unserialized.values)
|
| 700 |
+
|
| 701 |
+
def test_series_default_orient(self, string_series):
|
| 702 |
+
assert string_series.to_json() == string_series.to_json(orient="index")
|
| 703 |
+
|
| 704 |
+
def test_series_roundtrip_simple(self, orient, string_series, using_infer_string):
|
| 705 |
+
data = StringIO(string_series.to_json(orient=orient))
|
| 706 |
+
result = read_json(data, typ="series", orient=orient)
|
| 707 |
+
|
| 708 |
+
expected = string_series
|
| 709 |
+
if using_infer_string and orient in ("split", "index", "columns"):
|
| 710 |
+
# These schemas don't contain dtypes, so we infer string
|
| 711 |
+
expected.index = expected.index.astype("str")
|
| 712 |
+
if orient in ("values", "records"):
|
| 713 |
+
expected = expected.reset_index(drop=True)
|
| 714 |
+
if orient != "split":
|
| 715 |
+
expected.name = None
|
| 716 |
+
|
| 717 |
+
tm.assert_series_equal(result, expected)
|
| 718 |
+
|
| 719 |
+
@pytest.mark.parametrize("dtype", [False, None])
|
| 720 |
+
def test_series_roundtrip_object(self, orient, dtype, object_series):
|
| 721 |
+
data = StringIO(object_series.to_json(orient=orient))
|
| 722 |
+
result = read_json(data, typ="series", orient=orient, dtype=dtype)
|
| 723 |
+
|
| 724 |
+
expected = object_series
|
| 725 |
+
if orient in ("values", "records"):
|
| 726 |
+
expected = expected.reset_index(drop=True)
|
| 727 |
+
if orient != "split":
|
| 728 |
+
expected.name = None
|
| 729 |
+
|
| 730 |
+
if using_string_dtype():
|
| 731 |
+
expected = expected.astype("str")
|
| 732 |
+
|
| 733 |
+
tm.assert_series_equal(result, expected)
|
| 734 |
+
|
| 735 |
+
def test_series_roundtrip_empty(self, orient):
|
| 736 |
+
empty_series = Series([], index=[], dtype=np.float64)
|
| 737 |
+
data = StringIO(empty_series.to_json(orient=orient))
|
| 738 |
+
result = read_json(data, typ="series", orient=orient)
|
| 739 |
+
|
| 740 |
+
expected = empty_series.reset_index(drop=True)
|
| 741 |
+
if orient in ("split"):
|
| 742 |
+
expected.index = expected.index.astype(np.float64)
|
| 743 |
+
|
| 744 |
+
tm.assert_series_equal(result, expected)
|
| 745 |
+
|
| 746 |
+
@pytest.mark.parametrize(
|
| 747 |
+
"dtype_backend", [None, pytest.param("pyarrow", marks=td.skip_if_no("pyarrow"))]
|
| 748 |
+
)
|
| 749 |
+
def test_series_roundtrip_timeseries(self, dtype_backend, orient, datetime_series):
|
| 750 |
+
expected = datetime_series.copy()
|
| 751 |
+
if dtype_backend is not None:
|
| 752 |
+
datetime_series.index = Series(datetime_series.index).convert_dtypes(
|
| 753 |
+
dtype_backend=dtype_backend
|
| 754 |
+
)
|
| 755 |
+
|
| 756 |
+
data = StringIO(datetime_series.to_json(orient=orient))
|
| 757 |
+
result = read_json(data, typ="series", orient=orient)
|
| 758 |
+
|
| 759 |
+
if orient in ("values", "records"):
|
| 760 |
+
expected = expected.reset_index(drop=True)
|
| 761 |
+
else:
|
| 762 |
+
expected.index = expected.index.as_unit("ms")
|
| 763 |
+
if orient != "split":
|
| 764 |
+
expected.name = None
|
| 765 |
+
|
| 766 |
+
tm.assert_series_equal(result, expected)
|
| 767 |
+
|
| 768 |
+
@pytest.mark.parametrize("dtype", [np.float64, int])
|
| 769 |
+
def test_series_roundtrip_numeric(self, orient, dtype):
|
| 770 |
+
s = Series(range(6), index=["a", "b", "c", "d", "e", "f"])
|
| 771 |
+
data = StringIO(s.to_json(orient=orient))
|
| 772 |
+
result = read_json(data, typ="series", orient=orient)
|
| 773 |
+
|
| 774 |
+
expected = s.copy()
|
| 775 |
+
if orient in ("values", "records"):
|
| 776 |
+
expected = expected.reset_index(drop=True)
|
| 777 |
+
|
| 778 |
+
tm.assert_series_equal(result, expected)
|
| 779 |
+
|
| 780 |
+
def test_series_to_json_except(self):
|
| 781 |
+
s = Series([1, 2, 3])
|
| 782 |
+
msg = "Invalid value 'garbage' for option 'orient'"
|
| 783 |
+
with pytest.raises(ValueError, match=msg):
|
| 784 |
+
s.to_json(orient="garbage")
|
| 785 |
+
|
| 786 |
+
def test_series_from_json_precise_float(self):
|
| 787 |
+
s = Series([4.56, 4.56, 4.56])
|
| 788 |
+
result = read_json(StringIO(s.to_json()), typ="series", precise_float=True)
|
| 789 |
+
tm.assert_series_equal(result, s, check_index_type=False)
|
| 790 |
+
|
| 791 |
+
def test_series_with_dtype(self):
|
| 792 |
+
# GH 21986
|
| 793 |
+
s = Series([4.56, 4.56, 4.56])
|
| 794 |
+
result = read_json(StringIO(s.to_json()), typ="series", dtype=np.int64)
|
| 795 |
+
expected = Series([4] * 3)
|
| 796 |
+
tm.assert_series_equal(result, expected)
|
| 797 |
+
|
| 798 |
+
@pytest.mark.parametrize(
|
| 799 |
+
"dtype_backend", [None, pytest.param("pyarrow", marks=td.skip_if_no("pyarrow"))]
|
| 800 |
+
)
|
| 801 |
+
@pytest.mark.parametrize(
|
| 802 |
+
"dtype,expected",
|
| 803 |
+
[
|
| 804 |
+
(True, Series(["2000-01-01"], dtype="datetime64[ms]")),
|
| 805 |
+
(False, Series([946684800000])),
|
| 806 |
+
],
|
| 807 |
+
)
|
| 808 |
+
def test_series_with_dtype_datetime(self, dtype, expected, dtype_backend):
|
| 809 |
+
s = Series(["2000-01-01"], dtype="datetime64[ns]")
|
| 810 |
+
if dtype_backend is not None:
|
| 811 |
+
s = s.convert_dtypes(dtype_backend=dtype_backend)
|
| 812 |
+
msg = (
|
| 813 |
+
"The default 'epoch' date format is deprecated and will be removed "
|
| 814 |
+
"in a future version, please use 'iso' date format instead."
|
| 815 |
+
)
|
| 816 |
+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
|
| 817 |
+
data = StringIO(s.to_json())
|
| 818 |
+
result = read_json(data, typ="series", dtype=dtype)
|
| 819 |
+
tm.assert_series_equal(result, expected)
|
| 820 |
+
|
| 821 |
+
def test_frame_from_json_precise_float(self):
|
| 822 |
+
df = DataFrame([[4.56, 4.56, 4.56], [4.56, 4.56, 4.56]])
|
| 823 |
+
result = read_json(StringIO(df.to_json()), precise_float=True)
|
| 824 |
+
tm.assert_frame_equal(result, df)
|
| 825 |
+
|
| 826 |
+
def test_typ(self):
|
| 827 |
+
s = Series(range(6), index=["a", "b", "c", "d", "e", "f"], dtype="int64")
|
| 828 |
+
result = read_json(StringIO(s.to_json()), typ="series")
|
| 829 |
+
tm.assert_series_equal(result, s)
|
| 830 |
+
|
| 831 |
+
def test_reconstruction_index(self):
|
| 832 |
+
df = DataFrame([[1, 2, 3], [4, 5, 6]])
|
| 833 |
+
result = read_json(StringIO(df.to_json()))
|
| 834 |
+
tm.assert_frame_equal(result, df)
|
| 835 |
+
|
| 836 |
+
df = DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}, index=["A", "B", "C"])
|
| 837 |
+
result = read_json(StringIO(df.to_json()))
|
| 838 |
+
tm.assert_frame_equal(result, df)
|
| 839 |
+
|
| 840 |
+
def test_path(self, float_frame, int_frame, datetime_frame, temp_file):
|
| 841 |
+
for df in [float_frame, int_frame, datetime_frame]:
|
| 842 |
+
df.to_json(temp_file)
|
| 843 |
+
read_json(temp_file)
|
| 844 |
+
|
| 845 |
+
def test_axis_dates(self, datetime_series, datetime_frame):
|
| 846 |
+
# frame
|
| 847 |
+
json = StringIO(datetime_frame.to_json())
|
| 848 |
+
result = read_json(json)
|
| 849 |
+
expected = datetime_frame.copy()
|
| 850 |
+
expected.index = expected.index.as_unit("ms")
|
| 851 |
+
tm.assert_frame_equal(result, expected)
|
| 852 |
+
|
| 853 |
+
# series
|
| 854 |
+
json = StringIO(datetime_series.to_json())
|
| 855 |
+
result = read_json(json, typ="series")
|
| 856 |
+
expected = datetime_series.copy()
|
| 857 |
+
expected.index = expected.index.as_unit("ms")
|
| 858 |
+
tm.assert_series_equal(result, expected, check_names=False)
|
| 859 |
+
assert result.name is None
|
| 860 |
+
|
| 861 |
+
def test_convert_dates(self, datetime_series, datetime_frame):
|
| 862 |
+
# frame
|
| 863 |
+
df = datetime_frame
|
| 864 |
+
df["date"] = Timestamp("20130101").as_unit("ns")
|
| 865 |
+
|
| 866 |
+
msg = (
|
| 867 |
+
"The default 'epoch' date format is deprecated and will be removed "
|
| 868 |
+
"in a future version, please use 'iso' date format instead."
|
| 869 |
+
)
|
| 870 |
+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
|
| 871 |
+
json = StringIO(df.to_json())
|
| 872 |
+
result = read_json(json)
|
| 873 |
+
expected = df.copy()
|
| 874 |
+
expected["date"] = expected["date"].dt.as_unit("ms")
|
| 875 |
+
expected.index = expected.index.as_unit("ms")
|
| 876 |
+
tm.assert_frame_equal(result, expected)
|
| 877 |
+
|
| 878 |
+
df["foo"] = 1.0
|
| 879 |
+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
|
| 880 |
+
json = StringIO(df.to_json(date_unit="ns"))
|
| 881 |
+
|
| 882 |
+
result = read_json(json, convert_dates=False)
|
| 883 |
+
expected = df.copy()
|
| 884 |
+
expected["date"] = expected["date"].values.view("i8")
|
| 885 |
+
expected["foo"] = expected["foo"].astype("int64")
|
| 886 |
+
tm.assert_frame_equal(result, expected)
|
| 887 |
+
|
| 888 |
+
# series
|
| 889 |
+
ts = Series(Timestamp("20130101").as_unit("ns"), index=datetime_series.index)
|
| 890 |
+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
|
| 891 |
+
json = StringIO(ts.to_json())
|
| 892 |
+
result = read_json(json, typ="series")
|
| 893 |
+
expected = ts.dt.as_unit("ms")
|
| 894 |
+
expected.index = expected.index.as_unit("ms")
|
| 895 |
+
tm.assert_series_equal(result, expected)
|
| 896 |
+
|
| 897 |
+
@pytest.mark.parametrize("date_format", ["epoch", "iso"])
|
| 898 |
+
@pytest.mark.parametrize("as_object", [True, False])
|
| 899 |
+
@pytest.mark.parametrize("date_typ", [datetime.date, datetime.datetime, Timestamp])
|
| 900 |
+
def test_date_index_and_values(self, date_format, as_object, date_typ):
|
| 901 |
+
data = [date_typ(year=2020, month=1, day=1), pd.NaT]
|
| 902 |
+
if as_object:
|
| 903 |
+
data.append("a")
|
| 904 |
+
|
| 905 |
+
ser = Series(data, index=data)
|
| 906 |
+
if not as_object:
|
| 907 |
+
ser = ser.astype("M8[ns]")
|
| 908 |
+
if isinstance(ser.index, DatetimeIndex):
|
| 909 |
+
ser.index = ser.index.as_unit("ns")
|
| 910 |
+
|
| 911 |
+
expected_warning = None
|
| 912 |
+
if date_format == "epoch":
|
| 913 |
+
expected = '{"1577836800000":1577836800000,"null":null}'
|
| 914 |
+
expected_warning = Pandas4Warning
|
| 915 |
+
else:
|
| 916 |
+
expected = (
|
| 917 |
+
'{"2020-01-01T00:00:00.000":"2020-01-01T00:00:00.000","null":null}'
|
| 918 |
+
)
|
| 919 |
+
|
| 920 |
+
msg = (
|
| 921 |
+
"'epoch' date format is deprecated and will be removed in a future "
|
| 922 |
+
"version, please use 'iso' date format instead."
|
| 923 |
+
)
|
| 924 |
+
with tm.assert_produces_warning(expected_warning, match=msg):
|
| 925 |
+
result = ser.to_json(date_format=date_format)
|
| 926 |
+
|
| 927 |
+
if as_object:
|
| 928 |
+
expected = expected.replace("}", ',"a":"a"}')
|
| 929 |
+
|
| 930 |
+
assert result == expected
|
| 931 |
+
|
| 932 |
+
@pytest.mark.parametrize(
|
| 933 |
+
"infer_word",
|
| 934 |
+
[
|
| 935 |
+
"trade_time",
|
| 936 |
+
"date",
|
| 937 |
+
"datetime",
|
| 938 |
+
"sold_at",
|
| 939 |
+
"modified",
|
| 940 |
+
"timestamp",
|
| 941 |
+
"timestamps",
|
| 942 |
+
],
|
| 943 |
+
)
|
| 944 |
+
def test_convert_dates_infer(self, infer_word):
|
| 945 |
+
# GH10747
|
| 946 |
+
|
| 947 |
+
data = [{"id": 1, infer_word: 1036713600000}, {"id": 2}]
|
| 948 |
+
expected = DataFrame(
|
| 949 |
+
[[1, Timestamp("2002-11-08")], [2, pd.NaT]], columns=["id", infer_word]
|
| 950 |
+
)
|
| 951 |
+
expected[infer_word] = expected[infer_word].astype("M8[ms]")
|
| 952 |
+
|
| 953 |
+
result = read_json(StringIO(ujson_dumps(data)))[["id", infer_word]]
|
| 954 |
+
tm.assert_frame_equal(result, expected)
|
| 955 |
+
|
| 956 |
+
@pytest.mark.parametrize(
|
| 957 |
+
"date,date_unit",
|
| 958 |
+
[
|
| 959 |
+
("20130101 20:43:42.123", None),
|
| 960 |
+
("20130101 20:43:42", "s"),
|
| 961 |
+
("20130101 20:43:42.123", "ms"),
|
| 962 |
+
("20130101 20:43:42.123456", "us"),
|
| 963 |
+
("20130101 20:43:42.123456789", "ns"),
|
| 964 |
+
],
|
| 965 |
+
)
|
| 966 |
+
def test_date_format_frame(self, date, date_unit, datetime_frame):
|
| 967 |
+
df = datetime_frame
|
| 968 |
+
|
| 969 |
+
df["date"] = Timestamp(date).as_unit("ns")
|
| 970 |
+
df.iloc[1, df.columns.get_loc("date")] = pd.NaT
|
| 971 |
+
df.iloc[5, df.columns.get_loc("date")] = pd.NaT
|
| 972 |
+
if date_unit:
|
| 973 |
+
json = df.to_json(date_format="iso", date_unit=date_unit)
|
| 974 |
+
else:
|
| 975 |
+
json = df.to_json(date_format="iso")
|
| 976 |
+
|
| 977 |
+
result = read_json(StringIO(json))
|
| 978 |
+
expected = df.copy()
|
| 979 |
+
if date_unit != "ns":
|
| 980 |
+
expected["date"] = expected["date"].dt.as_unit("us")
|
| 981 |
+
expected.index = expected.index.as_unit("us")
|
| 982 |
+
tm.assert_frame_equal(result, expected)
|
| 983 |
+
|
| 984 |
+
def test_date_format_frame_raises(self, datetime_frame):
|
| 985 |
+
df = datetime_frame
|
| 986 |
+
msg = "Invalid value 'foo' for option 'date_unit'"
|
| 987 |
+
with pytest.raises(ValueError, match=msg):
|
| 988 |
+
df.to_json(date_format="iso", date_unit="foo")
|
| 989 |
+
|
| 990 |
+
@pytest.mark.parametrize(
|
| 991 |
+
"date,date_unit",
|
| 992 |
+
[
|
| 993 |
+
("20130101 20:43:42.123", None),
|
| 994 |
+
("20130101 20:43:42", "s"),
|
| 995 |
+
("20130101 20:43:42.123", "ms"),
|
| 996 |
+
("20130101 20:43:42.123456", "us"),
|
| 997 |
+
("20130101 20:43:42.123456789", "ns"),
|
| 998 |
+
],
|
| 999 |
+
)
|
| 1000 |
+
def test_date_format_series(self, date, date_unit, datetime_series):
|
| 1001 |
+
ts = Series(Timestamp(date), index=datetime_series.index)
|
| 1002 |
+
ts.iloc[1] = pd.NaT
|
| 1003 |
+
ts.iloc[5] = pd.NaT
|
| 1004 |
+
if date_unit:
|
| 1005 |
+
json = ts.to_json(date_format="iso", date_unit=date_unit)
|
| 1006 |
+
else:
|
| 1007 |
+
json = ts.to_json(date_format="iso")
|
| 1008 |
+
|
| 1009 |
+
result = read_json(StringIO(json), typ="series")
|
| 1010 |
+
expected = ts.copy()
|
| 1011 |
+
if date_unit != "ns":
|
| 1012 |
+
expected.index = expected.index.as_unit("us")
|
| 1013 |
+
tm.assert_series_equal(result, expected)
|
| 1014 |
+
|
| 1015 |
+
def test_date_format_series_raises(self, datetime_series):
|
| 1016 |
+
ts = Series(Timestamp("20130101 20:43:42.123"), index=datetime_series.index)
|
| 1017 |
+
msg = "Invalid value 'foo' for option 'date_unit'"
|
| 1018 |
+
with pytest.raises(ValueError, match=msg):
|
| 1019 |
+
ts.to_json(date_format="iso", date_unit="foo")
|
| 1020 |
+
|
| 1021 |
+
def test_date_unit(self, unit, datetime_frame):
|
| 1022 |
+
df = datetime_frame
|
| 1023 |
+
df["date"] = Timestamp("20130101 20:43:42").as_unit("ns")
|
| 1024 |
+
dl = df.columns.get_loc("date")
|
| 1025 |
+
df.iloc[1, dl] = Timestamp("19710101 20:43:42")
|
| 1026 |
+
df.iloc[2, dl] = Timestamp("21460101 20:43:42")
|
| 1027 |
+
df.iloc[4, dl] = pd.NaT
|
| 1028 |
+
|
| 1029 |
+
msg = (
|
| 1030 |
+
"'epoch' date format is deprecated and will be removed in a future "
|
| 1031 |
+
"version, please use 'iso' date format instead."
|
| 1032 |
+
)
|
| 1033 |
+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
|
| 1034 |
+
json = df.to_json(date_format="epoch", date_unit=unit)
|
| 1035 |
+
|
| 1036 |
+
# force date unit
|
| 1037 |
+
# result = read_json(StringIO(json), date_unit=unit)
|
| 1038 |
+
expected = df.copy()
|
| 1039 |
+
expected["date"] = expected["date"].dt.as_unit(unit)
|
| 1040 |
+
expected.index = expected.index.as_unit(unit)
|
| 1041 |
+
# tm.assert_frame_equal(result, expected)
|
| 1042 |
+
|
| 1043 |
+
# detect date unit
|
| 1044 |
+
result = read_json(StringIO(json), date_unit=None)
|
| 1045 |
+
tm.assert_frame_equal(result, expected)
|
| 1046 |
+
|
| 1047 |
+
@pytest.mark.parametrize(
|
| 1048 |
+
"df, warn",
|
| 1049 |
+
[
|
| 1050 |
+
(DataFrame({"A": ["a", "b", "c"], "B": np.arange(3)}), None),
|
| 1051 |
+
(DataFrame({"A": [True, False, False]}), None),
|
| 1052 |
+
(
|
| 1053 |
+
DataFrame(
|
| 1054 |
+
{"A": ["a", "b", "c"], "B": pd.to_timedelta(np.arange(3), unit="D")}
|
| 1055 |
+
),
|
| 1056 |
+
Pandas4Warning,
|
| 1057 |
+
),
|
| 1058 |
+
(
|
| 1059 |
+
DataFrame(
|
| 1060 |
+
{"A": pd.to_datetime(["2020-01-01", "2020-02-01", "2020-03-01"])}
|
| 1061 |
+
),
|
| 1062 |
+
Pandas4Warning,
|
| 1063 |
+
),
|
| 1064 |
+
],
|
| 1065 |
+
)
|
| 1066 |
+
def test_default_epoch_date_format_deprecated(self, df, warn):
|
| 1067 |
+
# GH 57063
|
| 1068 |
+
msg = (
|
| 1069 |
+
"The default 'epoch' date format is deprecated and will be removed "
|
| 1070 |
+
"in a future version, please use 'iso' date format instead."
|
| 1071 |
+
)
|
| 1072 |
+
with tm.assert_produces_warning(warn, match=msg):
|
| 1073 |
+
df.to_json()
|
| 1074 |
+
|
| 1075 |
+
@pytest.mark.parametrize(
|
| 1076 |
+
"dtype_backend", [None, pytest.param("pyarrow", marks=td.skip_if_no("pyarrow"))]
|
| 1077 |
+
)
|
| 1078 |
+
@pytest.mark.parametrize("unit", ["s", "ms", "us"])
|
| 1079 |
+
def test_iso_non_nano_datetimes(self, dtype_backend, unit):
|
| 1080 |
+
# Test that numpy datetimes
|
| 1081 |
+
# in an Index or a column with non-nano resolution can be serialized
|
| 1082 |
+
# correctly
|
| 1083 |
+
# GH53686
|
| 1084 |
+
index = DatetimeIndex(
|
| 1085 |
+
[np.datetime64("2023-01-01T11:22:33.123456", unit)],
|
| 1086 |
+
dtype=f"datetime64[{unit}]",
|
| 1087 |
+
)
|
| 1088 |
+
df = DataFrame(
|
| 1089 |
+
{
|
| 1090 |
+
"date": Series(
|
| 1091 |
+
[np.datetime64("2022-01-01T11:22:33.123456", unit)],
|
| 1092 |
+
dtype=f"datetime64[{unit}]",
|
| 1093 |
+
index=index,
|
| 1094 |
+
),
|
| 1095 |
+
"date_obj": Series(
|
| 1096 |
+
[np.datetime64("2023-01-01T11:22:33.123456", unit)],
|
| 1097 |
+
dtype=object,
|
| 1098 |
+
index=index,
|
| 1099 |
+
),
|
| 1100 |
+
},
|
| 1101 |
+
)
|
| 1102 |
+
expected = df.copy()
|
| 1103 |
+
expected.index = expected.index.as_unit("us")
|
| 1104 |
+
expected["date"] = expected["date"].dt.as_unit("us")
|
| 1105 |
+
expected["date_obj"] = expected["date_obj"].astype("datetime64[us]")
|
| 1106 |
+
|
| 1107 |
+
if dtype_backend is not None:
|
| 1108 |
+
df = df.convert_dtypes(dtype_backend=dtype_backend)
|
| 1109 |
+
|
| 1110 |
+
json = df.to_json(date_format="iso", date_unit=unit)
|
| 1111 |
+
result = read_json(StringIO(json), convert_dates=["date", "date_obj"])
|
| 1112 |
+
tm.assert_frame_equal(result, expected)
|
| 1113 |
+
|
| 1114 |
+
def test_weird_nested_json(self):
|
| 1115 |
+
# this used to core dump the parser
|
| 1116 |
+
s = r"""{
|
| 1117 |
+
"status": "success",
|
| 1118 |
+
"data": {
|
| 1119 |
+
"posts": [
|
| 1120 |
+
{
|
| 1121 |
+
"id": 1,
|
| 1122 |
+
"title": "A blog post",
|
| 1123 |
+
"body": "Some useful content"
|
| 1124 |
+
},
|
| 1125 |
+
{
|
| 1126 |
+
"id": 2,
|
| 1127 |
+
"title": "Another blog post",
|
| 1128 |
+
"body": "More content"
|
| 1129 |
+
}
|
| 1130 |
+
]
|
| 1131 |
+
}
|
| 1132 |
+
}"""
|
| 1133 |
+
read_json(StringIO(s))
|
| 1134 |
+
|
| 1135 |
+
def test_doc_example(self):
|
| 1136 |
+
dfj2 = DataFrame(
|
| 1137 |
+
np.random.default_rng(2).standard_normal((5, 2)), columns=list("AB")
|
| 1138 |
+
)
|
| 1139 |
+
dfj2["date"] = Timestamp("20130101")
|
| 1140 |
+
dfj2["ints"] = range(5)
|
| 1141 |
+
dfj2["bools"] = True
|
| 1142 |
+
dfj2.index = date_range("20130101", periods=5, unit="ns")
|
| 1143 |
+
|
| 1144 |
+
msg = (
|
| 1145 |
+
"The default 'epoch' date format is deprecated and will be removed "
|
| 1146 |
+
"in a future version, please use 'iso' date format instead."
|
| 1147 |
+
)
|
| 1148 |
+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
|
| 1149 |
+
json = StringIO(dfj2.to_json())
|
| 1150 |
+
result = read_json(json, dtype={"ints": np.int64, "bools": np.bool_})
|
| 1151 |
+
tm.assert_frame_equal(result, result)
|
| 1152 |
+
|
| 1153 |
+
def test_round_trip_exception(self, datapath):
|
| 1154 |
+
# GH 3867
|
| 1155 |
+
path = datapath("io", "json", "data", "teams.csv")
|
| 1156 |
+
df = pd.read_csv(path)
|
| 1157 |
+
s = df.to_json()
|
| 1158 |
+
|
| 1159 |
+
result = read_json(StringIO(s))
|
| 1160 |
+
res = result.reindex(index=df.index, columns=df.columns)
|
| 1161 |
+
res = res.fillna(np.nan)
|
| 1162 |
+
tm.assert_frame_equal(res, df)
|
| 1163 |
+
|
| 1164 |
+
@pytest.mark.network
|
| 1165 |
+
@pytest.mark.single_cpu
|
| 1166 |
+
def test_url(self, httpserver):
|
| 1167 |
+
data = '{"created_at": ["2023-06-23T18:21:36Z"], "closed_at": ["2023-06-23T18:21:36"], "updated_at": ["2023-06-23T18:21:36Z"]}\n' # noqa: E501
|
| 1168 |
+
httpserver.serve_content(content=data)
|
| 1169 |
+
result = read_json(httpserver.url, convert_dates=True)
|
| 1170 |
+
for field, dtype in [
|
| 1171 |
+
["created_at", pd.DatetimeTZDtype("us", tz="UTC")],
|
| 1172 |
+
["closed_at", "datetime64[us]"],
|
| 1173 |
+
["updated_at", pd.DatetimeTZDtype("us", tz="UTC")],
|
| 1174 |
+
]:
|
| 1175 |
+
assert result[field].dtype == dtype
|
| 1176 |
+
|
| 1177 |
+
def test_timedelta(self, unit):
|
| 1178 |
+
converter = lambda x: pd.to_timedelta(x, unit="ms")
|
| 1179 |
+
|
| 1180 |
+
ser = Series([timedelta(23), timedelta(seconds=5)], dtype=f"m8[{unit}]")
|
| 1181 |
+
assert ser.dtype == f"timedelta64[{unit}]"
|
| 1182 |
+
|
| 1183 |
+
msg = (
|
| 1184 |
+
"The default 'epoch' date format is deprecated and will be removed "
|
| 1185 |
+
"in a future version, please use 'iso' date format instead."
|
| 1186 |
+
)
|
| 1187 |
+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
|
| 1188 |
+
result = read_json(StringIO(ser.to_json()), typ="series").apply(converter)
|
| 1189 |
+
tm.assert_series_equal(result, ser.astype("m8[ms]"))
|
| 1190 |
+
|
| 1191 |
+
ser = Series(
|
| 1192 |
+
[timedelta(23), timedelta(seconds=5)], index=Index([0, 1]), dtype="m8[ns]"
|
| 1193 |
+
)
|
| 1194 |
+
assert ser.dtype == "timedelta64[ns]"
|
| 1195 |
+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
|
| 1196 |
+
result = read_json(StringIO(ser.to_json()), typ="series").apply(converter)
|
| 1197 |
+
tm.assert_series_equal(result, ser.astype("m8[ms]"))
|
| 1198 |
+
|
| 1199 |
+
frame = DataFrame([timedelta(23), timedelta(seconds=5)], dtype="m8[ns]")
|
| 1200 |
+
assert frame[0].dtype == "timedelta64[ns]"
|
| 1201 |
+
|
| 1202 |
+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
|
| 1203 |
+
json = frame.to_json()
|
| 1204 |
+
result = read_json(StringIO(json)).apply(converter)
|
| 1205 |
+
tm.assert_frame_equal(frame.astype("m8[ms]"), result)
|
| 1206 |
+
|
| 1207 |
+
def test_timedelta2(self):
|
| 1208 |
+
frame = DataFrame(
|
| 1209 |
+
{
|
| 1210 |
+
"a": [timedelta(days=23), timedelta(seconds=5)],
|
| 1211 |
+
"b": [1, 2],
|
| 1212 |
+
"c": date_range(start="20130101", periods=2, unit="ns"),
|
| 1213 |
+
}
|
| 1214 |
+
)
|
| 1215 |
+
frame["a"] = frame["a"].astype("m8[ns]")
|
| 1216 |
+
msg = (
|
| 1217 |
+
"The default 'epoch' date format is deprecated and will be removed "
|
| 1218 |
+
"in a future version, please use 'iso' date format instead."
|
| 1219 |
+
)
|
| 1220 |
+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
|
| 1221 |
+
data = StringIO(frame.to_json(date_unit="ns"))
|
| 1222 |
+
result = read_json(data)
|
| 1223 |
+
result["a"] = pd.to_timedelta(result.a, unit="ns")
|
| 1224 |
+
result["c"] = pd.to_datetime(result.c)
|
| 1225 |
+
tm.assert_frame_equal(frame, result)
|
| 1226 |
+
|
| 1227 |
+
def test_mixed_timedelta_datetime(self):
|
| 1228 |
+
td = timedelta(23)
|
| 1229 |
+
ts = Timestamp("20130101")
|
| 1230 |
+
frame = DataFrame({"a": [td, ts]}, dtype=object)
|
| 1231 |
+
|
| 1232 |
+
expected = DataFrame(
|
| 1233 |
+
{"a": [pd.Timedelta(td).as_unit("ns")._value, ts.as_unit("ns")._value]}
|
| 1234 |
+
)
|
| 1235 |
+
data = StringIO(frame.to_json(date_unit="ns"))
|
| 1236 |
+
result = read_json(data, dtype={"a": "int64"})
|
| 1237 |
+
tm.assert_frame_equal(result, expected, check_index_type=False)
|
| 1238 |
+
|
| 1239 |
+
@pytest.mark.parametrize("as_object", [True, False])
|
| 1240 |
+
@pytest.mark.parametrize("date_format", ["iso", "epoch"])
|
| 1241 |
+
@pytest.mark.parametrize("timedelta_typ", [pd.Timedelta, timedelta])
|
| 1242 |
+
def test_timedelta_to_json(self, as_object, date_format, timedelta_typ):
|
| 1243 |
+
# GH28156: to_json not correctly formatting Timedelta
|
| 1244 |
+
data = [timedelta_typ(days=1), timedelta_typ(days=2), pd.NaT]
|
| 1245 |
+
if as_object:
|
| 1246 |
+
data.append("a")
|
| 1247 |
+
|
| 1248 |
+
ser = Series(data, index=data)
|
| 1249 |
+
if not as_object:
|
| 1250 |
+
ser = ser.astype("m8[ns]")
|
| 1251 |
+
ser.index = ser.index.astype("m8[ns]")
|
| 1252 |
+
expected_warning = None
|
| 1253 |
+
if date_format == "iso":
|
| 1254 |
+
expected = (
|
| 1255 |
+
'{"P1DT0H0M0S":"P1DT0H0M0S","P2DT0H0M0S":"P2DT0H0M0S","null":null}'
|
| 1256 |
+
)
|
| 1257 |
+
else:
|
| 1258 |
+
expected_warning = Pandas4Warning
|
| 1259 |
+
expected = '{"86400000":86400000,"172800000":172800000,"null":null}'
|
| 1260 |
+
|
| 1261 |
+
if as_object:
|
| 1262 |
+
expected = expected.replace("}", ',"a":"a"}')
|
| 1263 |
+
|
| 1264 |
+
msg = (
|
| 1265 |
+
"'epoch' date format is deprecated and will be removed in a future "
|
| 1266 |
+
"version, please use 'iso' date format instead."
|
| 1267 |
+
)
|
| 1268 |
+
with tm.assert_produces_warning(expected_warning, match=msg):
|
| 1269 |
+
result = ser.to_json(date_format=date_format)
|
| 1270 |
+
assert result == expected
|
| 1271 |
+
|
| 1272 |
+
@pytest.mark.parametrize("as_object", [True, False])
|
| 1273 |
+
@pytest.mark.parametrize("timedelta_typ", [pd.Timedelta, timedelta])
|
| 1274 |
+
def test_timedelta_to_json_fractional_precision(self, as_object, timedelta_typ):
|
| 1275 |
+
data = [timedelta_typ(milliseconds=42)]
|
| 1276 |
+
ser = Series(data, index=data).astype("m8[ns]")
|
| 1277 |
+
ser.index = ser.index.astype("m8[ns]")
|
| 1278 |
+
warn = Pandas4Warning
|
| 1279 |
+
if as_object:
|
| 1280 |
+
ser = ser.astype(object)
|
| 1281 |
+
warn = None
|
| 1282 |
+
|
| 1283 |
+
msg = (
|
| 1284 |
+
"The default 'epoch' date format is deprecated and will be removed "
|
| 1285 |
+
"in a future version, please use 'iso' date format instead."
|
| 1286 |
+
)
|
| 1287 |
+
with tm.assert_produces_warning(warn, match=msg):
|
| 1288 |
+
result = ser.to_json()
|
| 1289 |
+
expected = '{"42":42}'
|
| 1290 |
+
assert result == expected
|
| 1291 |
+
|
| 1292 |
+
def test_default_handler(self):
|
| 1293 |
+
value = object()
|
| 1294 |
+
frame = DataFrame({"a": [7, value]})
|
| 1295 |
+
expected = DataFrame({"a": [7, str(value)]})
|
| 1296 |
+
result = read_json(StringIO(frame.to_json(default_handler=str)))
|
| 1297 |
+
tm.assert_frame_equal(expected, result, check_index_type=False)
|
| 1298 |
+
|
| 1299 |
+
def test_default_handler_indirect(self):
|
| 1300 |
+
def default(obj):
|
| 1301 |
+
if isinstance(obj, complex):
|
| 1302 |
+
return [("mathjs", "Complex"), ("re", obj.real), ("im", obj.imag)]
|
| 1303 |
+
return str(obj)
|
| 1304 |
+
|
| 1305 |
+
df_list = [
|
| 1306 |
+
9,
|
| 1307 |
+
DataFrame(
|
| 1308 |
+
{"a": [1, "STR", complex(4, -5)], "b": [float("nan"), None, "N/A"]},
|
| 1309 |
+
columns=["a", "b"],
|
| 1310 |
+
),
|
| 1311 |
+
]
|
| 1312 |
+
expected = (
|
| 1313 |
+
'[9,[[1,null],["STR",null],[[["mathjs","Complex"],'
|
| 1314 |
+
'["re",4.0],["im",-5.0]],"N\\/A"]]]'
|
| 1315 |
+
)
|
| 1316 |
+
assert (
|
| 1317 |
+
ujson_dumps(df_list, default_handler=default, orient="values") == expected
|
| 1318 |
+
)
|
| 1319 |
+
|
| 1320 |
+
def test_default_handler_numpy_unsupported_dtype(self):
|
| 1321 |
+
# GH12554 to_json raises 'Unhandled numpy dtype 15'
|
| 1322 |
+
df = DataFrame(
|
| 1323 |
+
{"a": [1, 2.3, complex(4, -5)], "b": [float("nan"), None, complex(1.2, 0)]},
|
| 1324 |
+
columns=["a", "b"],
|
| 1325 |
+
)
|
| 1326 |
+
expected = (
|
| 1327 |
+
'[["(1+0j)","(nan+0j)"],["(2.3+0j)","(nan+0j)"],["(4-5j)","(1.2+0j)"]]'
|
| 1328 |
+
)
|
| 1329 |
+
assert df.to_json(default_handler=str, orient="values") == expected
|
| 1330 |
+
|
| 1331 |
+
def test_default_handler_raises(self):
|
| 1332 |
+
msg = "raisin"
|
| 1333 |
+
|
| 1334 |
+
def my_handler_raises(obj):
|
| 1335 |
+
raise TypeError(msg)
|
| 1336 |
+
|
| 1337 |
+
with pytest.raises(TypeError, match=msg):
|
| 1338 |
+
DataFrame({"a": [1, 2, object()]}).to_json(
|
| 1339 |
+
default_handler=my_handler_raises
|
| 1340 |
+
)
|
| 1341 |
+
with pytest.raises(TypeError, match=msg):
|
| 1342 |
+
DataFrame({"a": [1, 2, complex(4, -5)]}).to_json(
|
| 1343 |
+
default_handler=my_handler_raises
|
| 1344 |
+
)
|
| 1345 |
+
|
| 1346 |
+
def test_categorical(self):
|
| 1347 |
+
# GH4377 df.to_json segfaults with non-ndarray blocks
|
| 1348 |
+
df = DataFrame({"A": ["a", "b", "c", "a", "b", "b", "a"]})
|
| 1349 |
+
df["B"] = df["A"]
|
| 1350 |
+
expected = df.to_json()
|
| 1351 |
+
|
| 1352 |
+
df["B"] = df["A"].astype("category")
|
| 1353 |
+
assert expected == df.to_json()
|
| 1354 |
+
|
| 1355 |
+
s = df["A"]
|
| 1356 |
+
sc = df["B"]
|
| 1357 |
+
assert s.to_json() == sc.to_json()
|
| 1358 |
+
|
| 1359 |
+
def test_datetime_tz(self):
|
| 1360 |
+
# GH4377 df.to_json segfaults with non-ndarray blocks
|
| 1361 |
+
tz_range = date_range("20130101", periods=3, tz="US/Eastern", unit="ns")
|
| 1362 |
+
tz_naive = tz_range.tz_convert("utc").tz_localize(None)
|
| 1363 |
+
|
| 1364 |
+
df = DataFrame(
|
| 1365 |
+
{"A": tz_range, "B": date_range("20130101", periods=3, unit="ns")}
|
| 1366 |
+
)
|
| 1367 |
+
|
| 1368 |
+
df_naive = df.copy()
|
| 1369 |
+
df_naive["A"] = tz_naive
|
| 1370 |
+
msg = (
|
| 1371 |
+
"The default 'epoch' date format is deprecated and will be removed "
|
| 1372 |
+
"in a future version, please use 'iso' date format instead."
|
| 1373 |
+
)
|
| 1374 |
+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
|
| 1375 |
+
expected = df_naive.to_json()
|
| 1376 |
+
assert expected == df.to_json()
|
| 1377 |
+
|
| 1378 |
+
stz = Series(tz_range)
|
| 1379 |
+
s_naive = Series(tz_naive)
|
| 1380 |
+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
|
| 1381 |
+
assert stz.to_json() == s_naive.to_json()
|
| 1382 |
+
|
| 1383 |
+
def test_sparse(self):
|
| 1384 |
+
# GH4377 df.to_json segfaults with non-ndarray blocks
|
| 1385 |
+
df = DataFrame(np.random.default_rng(2).standard_normal((10, 4)))
|
| 1386 |
+
df.loc[:8] = np.nan
|
| 1387 |
+
|
| 1388 |
+
sdf = df.astype("Sparse")
|
| 1389 |
+
expected = df.to_json()
|
| 1390 |
+
assert expected == sdf.to_json()
|
| 1391 |
+
|
| 1392 |
+
s = Series(np.random.default_rng(2).standard_normal(10))
|
| 1393 |
+
s.loc[:8] = np.nan
|
| 1394 |
+
ss = s.astype("Sparse")
|
| 1395 |
+
|
| 1396 |
+
expected = s.to_json()
|
| 1397 |
+
assert expected == ss.to_json()
|
| 1398 |
+
|
| 1399 |
+
@pytest.mark.parametrize(
|
| 1400 |
+
"ts",
|
| 1401 |
+
[
|
| 1402 |
+
Timestamp("2013-01-10 05:00:00Z"),
|
| 1403 |
+
Timestamp("2013-01-10 00:00:00", tz="US/Eastern"),
|
| 1404 |
+
Timestamp("2013-01-10 00:00:00-0500"),
|
| 1405 |
+
],
|
| 1406 |
+
)
|
| 1407 |
+
def test_tz_is_utc(self, ts):
|
| 1408 |
+
exp = '"2013-01-10T05:00:00.000Z"'
|
| 1409 |
+
|
| 1410 |
+
assert ujson_dumps(ts, iso_dates=True) == exp
|
| 1411 |
+
dt = ts.to_pydatetime()
|
| 1412 |
+
assert ujson_dumps(dt, iso_dates=True) == exp
|
| 1413 |
+
|
| 1414 |
+
def test_tz_is_naive(self):
|
| 1415 |
+
ts = Timestamp("2013-01-10 05:00:00")
|
| 1416 |
+
exp = '"2013-01-10T05:00:00.000"'
|
| 1417 |
+
|
| 1418 |
+
assert ujson_dumps(ts, iso_dates=True) == exp
|
| 1419 |
+
dt = ts.to_pydatetime()
|
| 1420 |
+
assert ujson_dumps(dt, iso_dates=True) == exp
|
| 1421 |
+
|
| 1422 |
+
@pytest.mark.parametrize(
|
| 1423 |
+
"tz_range",
|
| 1424 |
+
[
|
| 1425 |
+
date_range("2013-01-01 05:00:00Z", periods=2, unit="ns"),
|
| 1426 |
+
date_range("2013-01-01 00:00:00", periods=2, tz="US/Eastern", unit="ns"),
|
| 1427 |
+
date_range("2013-01-01 00:00:00-0500", periods=2, unit="ns"),
|
| 1428 |
+
],
|
| 1429 |
+
)
|
| 1430 |
+
def test_tz_range_is_utc(self, tz_range):
|
| 1431 |
+
exp = '["2013-01-01T05:00:00.000Z","2013-01-02T05:00:00.000Z"]'
|
| 1432 |
+
dfexp = '{"DT":{"0":"2013-01-01T05:00:00.000Z","1":"2013-01-02T05:00:00.000Z"}}'
|
| 1433 |
+
|
| 1434 |
+
assert ujson_dumps(tz_range, iso_dates=True) == exp
|
| 1435 |
+
dti = DatetimeIndex(tz_range)
|
| 1436 |
+
# Ensure datetimes in object array are serialized correctly
|
| 1437 |
+
# in addition to the normal DTI case
|
| 1438 |
+
assert ujson_dumps(dti, iso_dates=True) == exp
|
| 1439 |
+
assert ujson_dumps(dti.astype(object), iso_dates=True) == exp
|
| 1440 |
+
df = DataFrame({"DT": dti})
|
| 1441 |
+
result = ujson_dumps(df, iso_dates=True)
|
| 1442 |
+
assert result == dfexp
|
| 1443 |
+
assert ujson_dumps(df.astype({"DT": object}), iso_dates=True)
|
| 1444 |
+
|
| 1445 |
+
def test_tz_range_is_naive(self):
|
| 1446 |
+
dti = date_range("2013-01-01 05:00:00", periods=2, unit="ns")
|
| 1447 |
+
|
| 1448 |
+
exp = '["2013-01-01T05:00:00.000","2013-01-02T05:00:00.000"]'
|
| 1449 |
+
dfexp = '{"DT":{"0":"2013-01-01T05:00:00.000","1":"2013-01-02T05:00:00.000"}}'
|
| 1450 |
+
|
| 1451 |
+
# Ensure datetimes in object array are serialized correctly
|
| 1452 |
+
# in addition to the normal DTI case
|
| 1453 |
+
assert ujson_dumps(dti, iso_dates=True) == exp
|
| 1454 |
+
assert ujson_dumps(dti.astype(object), iso_dates=True) == exp
|
| 1455 |
+
df = DataFrame({"DT": dti})
|
| 1456 |
+
result = ujson_dumps(df, iso_dates=True)
|
| 1457 |
+
assert result == dfexp
|
| 1458 |
+
assert ujson_dumps(df.astype({"DT": object}), iso_dates=True)
|
| 1459 |
+
|
| 1460 |
+
def test_read_inline_jsonl(self):
|
| 1461 |
+
# GH9180
|
| 1462 |
+
|
| 1463 |
+
result = read_json(StringIO('{"a": 1, "b": 2}\n{"b":2, "a" :1}\n'), lines=True)
|
| 1464 |
+
expected = DataFrame([[1, 2], [1, 2]], columns=["a", "b"])
|
| 1465 |
+
tm.assert_frame_equal(result, expected)
|
| 1466 |
+
|
| 1467 |
+
@pytest.mark.single_cpu
|
| 1468 |
+
@pytest.mark.network
|
| 1469 |
+
@td.skip_if_not_us_locale
|
| 1470 |
+
def test_read_s3_jsonl(self, s3_bucket_public_with_data, s3so):
|
| 1471 |
+
# GH17200
|
| 1472 |
+
result = read_json(
|
| 1473 |
+
f"s3n://{s3_bucket_public_with_data.name}/items.jsonl",
|
| 1474 |
+
lines=True,
|
| 1475 |
+
storage_options=s3so,
|
| 1476 |
+
)
|
| 1477 |
+
expected = DataFrame([[1, 2], [1, 2]], columns=["a", "b"])
|
| 1478 |
+
tm.assert_frame_equal(result, expected)
|
| 1479 |
+
|
| 1480 |
+
def test_read_local_jsonl(self, temp_file):
|
| 1481 |
+
# GH17200
|
| 1482 |
+
with open(temp_file, "w", encoding="utf-8") as infile:
|
| 1483 |
+
infile.write('{"a": 1, "b": 2}\n{"b":2, "a" :1}\n')
|
| 1484 |
+
result = read_json(temp_file, lines=True)
|
| 1485 |
+
expected = DataFrame([[1, 2], [1, 2]], columns=["a", "b"])
|
| 1486 |
+
tm.assert_frame_equal(result, expected)
|
| 1487 |
+
|
| 1488 |
+
def test_read_jsonl_unicode_chars(self):
|
| 1489 |
+
# GH15132: non-ascii unicode characters
|
| 1490 |
+
# \u201d == RIGHT DOUBLE QUOTATION MARK
|
| 1491 |
+
|
| 1492 |
+
# simulate file handle
|
| 1493 |
+
json = '{"a": "foo”", "b": "bar"}\n{"a": "foo", "b": "bar"}\n'
|
| 1494 |
+
json = StringIO(json)
|
| 1495 |
+
result = read_json(json, lines=True)
|
| 1496 |
+
expected = DataFrame([["foo\u201d", "bar"], ["foo", "bar"]], columns=["a", "b"])
|
| 1497 |
+
tm.assert_frame_equal(result, expected)
|
| 1498 |
+
|
| 1499 |
+
# simulate string
|
| 1500 |
+
json = StringIO('{"a": "foo”", "b": "bar"}\n{"a": "foo", "b": "bar"}\n')
|
| 1501 |
+
result = read_json(json, lines=True)
|
| 1502 |
+
expected = DataFrame([["foo\u201d", "bar"], ["foo", "bar"]], columns=["a", "b"])
|
| 1503 |
+
tm.assert_frame_equal(result, expected)
|
| 1504 |
+
|
| 1505 |
+
@pytest.mark.parametrize("bigNum", [sys.maxsize + 1, -(sys.maxsize + 2)])
|
| 1506 |
+
def test_to_json_large_numbers(self, bigNum):
|
| 1507 |
+
# GH34473
|
| 1508 |
+
series = Series(bigNum, dtype=object, index=["articleId"])
|
| 1509 |
+
json = series.to_json()
|
| 1510 |
+
expected = '{"articleId":' + str(bigNum) + "}"
|
| 1511 |
+
assert json == expected
|
| 1512 |
+
|
| 1513 |
+
df = DataFrame(bigNum, dtype=object, index=["articleId"], columns=[0])
|
| 1514 |
+
json = df.to_json()
|
| 1515 |
+
expected = '{"0":{"articleId":' + str(bigNum) + "}}"
|
| 1516 |
+
assert json == expected
|
| 1517 |
+
|
| 1518 |
+
@pytest.mark.parametrize("bigNum", [-(2**63) - 1, 2**64])
|
| 1519 |
+
def test_read_json_large_numbers(self, bigNum):
|
| 1520 |
+
# GH20599, 26068
|
| 1521 |
+
json = StringIO('{"articleId":' + str(bigNum) + "}")
|
| 1522 |
+
msg = r"Value is too small|Value is too big"
|
| 1523 |
+
with pytest.raises(ValueError, match=msg):
|
| 1524 |
+
read_json(json)
|
| 1525 |
+
|
| 1526 |
+
json = StringIO('{"0":{"articleId":' + str(bigNum) + "}}")
|
| 1527 |
+
with pytest.raises(ValueError, match=msg):
|
| 1528 |
+
read_json(json)
|
| 1529 |
+
|
| 1530 |
+
def test_read_json_large_numbers2(self):
|
| 1531 |
+
# GH18842
|
| 1532 |
+
json = '{"articleId": "1404366058080022500245"}'
|
| 1533 |
+
json = StringIO(json)
|
| 1534 |
+
result = read_json(json, typ="series")
|
| 1535 |
+
expected = Series(1.404366e21, index=["articleId"])
|
| 1536 |
+
tm.assert_series_equal(result, expected)
|
| 1537 |
+
|
| 1538 |
+
json = '{"0": {"articleId": "1404366058080022500245"}}'
|
| 1539 |
+
json = StringIO(json)
|
| 1540 |
+
result = read_json(json)
|
| 1541 |
+
expected = DataFrame(1.404366e21, index=["articleId"], columns=[0])
|
| 1542 |
+
tm.assert_frame_equal(result, expected)
|
| 1543 |
+
|
| 1544 |
+
def test_to_jsonl(self):
|
| 1545 |
+
# GH9180
|
| 1546 |
+
df = DataFrame([[1, 2], [1, 2]], columns=["a", "b"])
|
| 1547 |
+
result = df.to_json(orient="records", lines=True)
|
| 1548 |
+
expected = '{"a":1,"b":2}\n{"a":1,"b":2}\n'
|
| 1549 |
+
assert result == expected
|
| 1550 |
+
|
| 1551 |
+
df = DataFrame([["foo}", "bar"], ['foo"', "bar"]], columns=["a", "b"])
|
| 1552 |
+
result = df.to_json(orient="records", lines=True)
|
| 1553 |
+
expected = '{"a":"foo}","b":"bar"}\n{"a":"foo\\"","b":"bar"}\n'
|
| 1554 |
+
assert result == expected
|
| 1555 |
+
tm.assert_frame_equal(read_json(StringIO(result), lines=True), df)
|
| 1556 |
+
|
| 1557 |
+
# GH15096: escaped characters in columns and data
|
| 1558 |
+
df = DataFrame([["foo\\", "bar"], ['foo"', "bar"]], columns=["a\\", "b"])
|
| 1559 |
+
result = df.to_json(orient="records", lines=True)
|
| 1560 |
+
expected = '{"a\\\\":"foo\\\\","b":"bar"}\n{"a\\\\":"foo\\"","b":"bar"}\n'
|
| 1561 |
+
assert result == expected
|
| 1562 |
+
|
| 1563 |
+
tm.assert_frame_equal(read_json(StringIO(result), lines=True), df)
|
| 1564 |
+
|
| 1565 |
+
# TODO: there is a near-identical test for pytables; can we share?
|
| 1566 |
+
@pytest.mark.xfail(reason="GH#13774 encoding kwarg not supported", raises=TypeError)
|
| 1567 |
+
@pytest.mark.parametrize(
|
| 1568 |
+
"val",
|
| 1569 |
+
[
|
| 1570 |
+
[b"E\xc9, 17", b"", b"a", b"b", b"c"],
|
| 1571 |
+
[b"E\xc9, 17", b"a", b"b", b"c"],
|
| 1572 |
+
[b"EE, 17", b"", b"a", b"b", b"c"],
|
| 1573 |
+
[b"E\xc9, 17", b"\xf8\xfc", b"a", b"b", b"c"],
|
| 1574 |
+
[b"", b"a", b"b", b"c"],
|
| 1575 |
+
[b"\xf8\xfc", b"a", b"b", b"c"],
|
| 1576 |
+
[b"A\xf8\xfc", b"", b"a", b"b", b"c"],
|
| 1577 |
+
[np.nan, b"", b"b", b"c"],
|
| 1578 |
+
[b"A\xf8\xfc", np.nan, b"", b"b", b"c"],
|
| 1579 |
+
],
|
| 1580 |
+
)
|
| 1581 |
+
@pytest.mark.parametrize("dtype", ["category", object])
|
| 1582 |
+
def test_latin_encoding(self, dtype, val, temp_file):
|
| 1583 |
+
# GH 13774
|
| 1584 |
+
ser = Series(
|
| 1585 |
+
[x.decode("latin-1") if isinstance(x, bytes) else x for x in val],
|
| 1586 |
+
dtype=dtype,
|
| 1587 |
+
)
|
| 1588 |
+
encoding = "latin-1"
|
| 1589 |
+
ser.to_json(temp_file, encoding=encoding)
|
| 1590 |
+
retr = read_json(StringIO(temp_file), encoding=encoding)
|
| 1591 |
+
tm.assert_series_equal(ser, retr, check_categorical=False)
|
| 1592 |
+
|
| 1593 |
+
def test_data_frame_size_after_to_json(self):
|
| 1594 |
+
# GH15344
|
| 1595 |
+
df = DataFrame({"a": [str(1)]})
|
| 1596 |
+
|
| 1597 |
+
size_before = df.memory_usage(index=True, deep=True).sum()
|
| 1598 |
+
df.to_json()
|
| 1599 |
+
size_after = df.memory_usage(index=True, deep=True).sum()
|
| 1600 |
+
|
| 1601 |
+
assert size_before == size_after
|
| 1602 |
+
|
| 1603 |
+
@pytest.mark.parametrize(
|
| 1604 |
+
"index", [None, [1, 2], [1.0, 2.0], ["a", "b"], ["1", "2"], ["1.", "2."]]
|
| 1605 |
+
)
|
| 1606 |
+
@pytest.mark.parametrize("columns", [["a", "b"], ["1", "2"], ["1.", "2."]])
|
| 1607 |
+
def test_from_json_to_json_table_index_and_columns(self, index, columns):
|
| 1608 |
+
# GH25433 GH25435
|
| 1609 |
+
expected = DataFrame([[1, 2], [3, 4]], index=index, columns=columns)
|
| 1610 |
+
dfjson = expected.to_json(orient="table")
|
| 1611 |
+
|
| 1612 |
+
result = read_json(StringIO(dfjson), orient="table")
|
| 1613 |
+
tm.assert_frame_equal(result, expected)
|
| 1614 |
+
|
| 1615 |
+
def test_from_json_to_json_table_dtypes(self):
|
| 1616 |
+
# GH21345
|
| 1617 |
+
expected = DataFrame({"a": [1, 2], "b": [3.0, 4.0], "c": ["5", "6"]})
|
| 1618 |
+
dfjson = expected.to_json(orient="table")
|
| 1619 |
+
result = read_json(StringIO(dfjson), orient="table")
|
| 1620 |
+
tm.assert_frame_equal(result, expected)
|
| 1621 |
+
|
| 1622 |
+
@pytest.mark.parametrize("orient", ["split", "records", "index", "columns"])
|
| 1623 |
+
def test_to_json_from_json_columns_dtypes(self, orient, using_infer_string):
|
| 1624 |
+
# GH21892 GH33205
|
| 1625 |
+
expected = DataFrame.from_dict(
|
| 1626 |
+
{
|
| 1627 |
+
"Integer": Series([1, 2, 3], dtype="int64"),
|
| 1628 |
+
"Float": Series([None, 2.0, 3.0], dtype="float64"),
|
| 1629 |
+
"Object": Series([None, "", "c"], dtype="object"),
|
| 1630 |
+
"Bool": Series([True, False, True], dtype="bool"),
|
| 1631 |
+
"Category": Series(["a", "b", None], dtype="category"),
|
| 1632 |
+
"Datetime": Series(
|
| 1633 |
+
["2020-01-01", None, "2020-01-03"], dtype="datetime64[ms]"
|
| 1634 |
+
),
|
| 1635 |
+
}
|
| 1636 |
+
)
|
| 1637 |
+
msg = (
|
| 1638 |
+
"The default 'epoch' date format is deprecated and will be removed "
|
| 1639 |
+
"in a future version, please use 'iso' date format instead."
|
| 1640 |
+
)
|
| 1641 |
+
with tm.assert_produces_warning(Pandas4Warning, match=msg):
|
| 1642 |
+
dfjson = expected.to_json(orient=orient)
|
| 1643 |
+
|
| 1644 |
+
if using_infer_string:
|
| 1645 |
+
# When this is read back in it is inferred to "str" dtype which
|
| 1646 |
+
# uses NaN instead of None.
|
| 1647 |
+
expected.loc[0, "Object"] = np.nan
|
| 1648 |
+
|
| 1649 |
+
result = read_json(
|
| 1650 |
+
StringIO(dfjson),
|
| 1651 |
+
orient=orient,
|
| 1652 |
+
dtype={
|
| 1653 |
+
"Integer": "int64",
|
| 1654 |
+
"Float": "float64",
|
| 1655 |
+
"Object": "object",
|
| 1656 |
+
"Bool": "bool",
|
| 1657 |
+
"Category": "category",
|
| 1658 |
+
"Datetime": "datetime64[ns]",
|
| 1659 |
+
},
|
| 1660 |
+
)
|
| 1661 |
+
tm.assert_frame_equal(result, expected)
|
| 1662 |
+
|
| 1663 |
+
def test_to_json_with_index_as_a_column_name(self):
|
| 1664 |
+
df = DataFrame(data={"index": [1, 2], "a": [2, 3]})
|
| 1665 |
+
with pytest.raises(
|
| 1666 |
+
ValueError, match="Overlapping names between the index and columns"
|
| 1667 |
+
):
|
| 1668 |
+
df.to_json(orient="table")
|
| 1669 |
+
|
| 1670 |
+
@pytest.mark.parametrize("dtype", [True, {"b": int, "c": int}])
|
| 1671 |
+
def test_read_json_table_dtype_raises(self, dtype):
|
| 1672 |
+
# GH21345
|
| 1673 |
+
df = DataFrame({"a": [1, 2], "b": [3.0, 4.0], "c": ["5", "6"]})
|
| 1674 |
+
dfjson = df.to_json(orient="table")
|
| 1675 |
+
msg = "cannot pass both dtype and orient='table'"
|
| 1676 |
+
with pytest.raises(ValueError, match=msg):
|
| 1677 |
+
read_json(dfjson, orient="table", dtype=dtype)
|
| 1678 |
+
|
| 1679 |
+
@pytest.mark.parametrize("orient", ["index", "columns", "records", "values"])
|
| 1680 |
+
def test_read_json_table_empty_axes_dtype(self, orient):
|
| 1681 |
+
# GH28558
|
| 1682 |
+
|
| 1683 |
+
expected = DataFrame()
|
| 1684 |
+
result = read_json(StringIO("{}"), orient=orient, convert_axes=True)
|
| 1685 |
+
tm.assert_index_equal(result.index, expected.index)
|
| 1686 |
+
tm.assert_index_equal(result.columns, expected.columns)
|
| 1687 |
+
|
| 1688 |
+
def test_read_json_table_convert_axes_raises(self):
|
| 1689 |
+
# GH25433 GH25435
|
| 1690 |
+
df = DataFrame([[1, 2], [3, 4]], index=[1.0, 2.0], columns=["1.", "2."])
|
| 1691 |
+
dfjson = df.to_json(orient="table")
|
| 1692 |
+
msg = "cannot pass both convert_axes and orient='table'"
|
| 1693 |
+
with pytest.raises(ValueError, match=msg):
|
| 1694 |
+
read_json(dfjson, orient="table", convert_axes=True)
|
| 1695 |
+
|
| 1696 |
+
@pytest.mark.parametrize(
|
| 1697 |
+
"data, expected",
|
| 1698 |
+
[
|
| 1699 |
+
(
|
| 1700 |
+
DataFrame([[1, 2], [4, 5]], columns=["a", "b"]),
|
| 1701 |
+
{"columns": ["a", "b"], "data": [[1, 2], [4, 5]]},
|
| 1702 |
+
),
|
| 1703 |
+
(
|
| 1704 |
+
DataFrame([[1, 2], [4, 5]], columns=["a", "b"]).rename_axis("foo"),
|
| 1705 |
+
{"columns": ["a", "b"], "data": [[1, 2], [4, 5]]},
|
| 1706 |
+
),
|
| 1707 |
+
(
|
| 1708 |
+
DataFrame(
|
| 1709 |
+
[[1, 2], [4, 5]], columns=["a", "b"], index=[["a", "b"], ["c", "d"]]
|
| 1710 |
+
),
|
| 1711 |
+
{"columns": ["a", "b"], "data": [[1, 2], [4, 5]]},
|
| 1712 |
+
),
|
| 1713 |
+
(Series([1, 2, 3], name="A"), {"name": "A", "data": [1, 2, 3]}),
|
| 1714 |
+
(
|
| 1715 |
+
Series([1, 2, 3], name="A").rename_axis("foo"),
|
| 1716 |
+
{"name": "A", "data": [1, 2, 3]},
|
| 1717 |
+
),
|
| 1718 |
+
(
|
| 1719 |
+
Series([1, 2], name="A", index=[["a", "b"], ["c", "d"]]),
|
| 1720 |
+
{"name": "A", "data": [1, 2]},
|
| 1721 |
+
),
|
| 1722 |
+
],
|
| 1723 |
+
)
|
| 1724 |
+
def test_index_false_to_json_split(self, data, expected):
|
| 1725 |
+
# GH 17394
|
| 1726 |
+
# Testing index=False in to_json with orient='split'
|
| 1727 |
+
|
| 1728 |
+
result = data.to_json(orient="split", index=False)
|
| 1729 |
+
result = json.loads(result)
|
| 1730 |
+
|
| 1731 |
+
assert result == expected
|
| 1732 |
+
|
| 1733 |
+
@pytest.mark.parametrize(
|
| 1734 |
+
"data",
|
| 1735 |
+
[
|
| 1736 |
+
(DataFrame([[1, 2], [4, 5]], columns=["a", "b"])),
|
| 1737 |
+
(DataFrame([[1, 2], [4, 5]], columns=["a", "b"]).rename_axis("foo")),
|
| 1738 |
+
(
|
| 1739 |
+
DataFrame(
|
| 1740 |
+
[[1, 2], [4, 5]], columns=["a", "b"], index=[["a", "b"], ["c", "d"]]
|
| 1741 |
+
)
|
| 1742 |
+
),
|
| 1743 |
+
(Series([1, 2, 3], name="A")),
|
| 1744 |
+
(Series([1, 2, 3], name="A").rename_axis("foo")),
|
| 1745 |
+
(Series([1, 2], name="A", index=[["a", "b"], ["c", "d"]])),
|
| 1746 |
+
],
|
| 1747 |
+
)
|
| 1748 |
+
def test_index_false_to_json_table(self, data):
|
| 1749 |
+
# GH 17394
|
| 1750 |
+
# Testing index=False in to_json with orient='table'
|
| 1751 |
+
|
| 1752 |
+
result = data.to_json(orient="table", index=False)
|
| 1753 |
+
result = json.loads(result)
|
| 1754 |
+
|
| 1755 |
+
expected = {
|
| 1756 |
+
"schema": pd.io.json.build_table_schema(data, index=False),
|
| 1757 |
+
"data": DataFrame(data).to_dict(orient="records"),
|
| 1758 |
+
}
|
| 1759 |
+
|
| 1760 |
+
assert result == expected
|
| 1761 |
+
|
| 1762 |
+
@pytest.mark.parametrize("orient", ["index", "columns"])
|
| 1763 |
+
def test_index_false_error_to_json(self, orient):
|
| 1764 |
+
# GH 17394, 25513
|
| 1765 |
+
# Testing error message from to_json with index=False
|
| 1766 |
+
|
| 1767 |
+
df = DataFrame([[1, 2], [4, 5]], columns=["a", "b"])
|
| 1768 |
+
|
| 1769 |
+
msg = (
|
| 1770 |
+
"'index=False' is only valid when 'orient' is 'split', "
|
| 1771 |
+
"'table', 'records', or 'values'"
|
| 1772 |
+
)
|
| 1773 |
+
with pytest.raises(ValueError, match=msg):
|
| 1774 |
+
df.to_json(orient=orient, index=False)
|
| 1775 |
+
|
| 1776 |
+
@pytest.mark.parametrize("orient", ["records", "values"])
|
| 1777 |
+
def test_index_true_error_to_json(self, orient):
|
| 1778 |
+
# GH 25513
|
| 1779 |
+
# Testing error message from to_json with index=True
|
| 1780 |
+
|
| 1781 |
+
df = DataFrame([[1, 2], [4, 5]], columns=["a", "b"])
|
| 1782 |
+
|
| 1783 |
+
msg = (
|
| 1784 |
+
"'index=True' is only valid when 'orient' is 'split', "
|
| 1785 |
+
"'table', 'index', or 'columns'"
|
| 1786 |
+
)
|
| 1787 |
+
with pytest.raises(ValueError, match=msg):
|
| 1788 |
+
df.to_json(orient=orient, index=True)
|
| 1789 |
+
|
| 1790 |
+
@pytest.mark.parametrize("orient", ["split", "table"])
|
| 1791 |
+
@pytest.mark.parametrize("index", [True, False])
|
| 1792 |
+
def test_index_false_from_json_to_json(self, orient, index):
|
| 1793 |
+
# GH25170
|
| 1794 |
+
# Test index=False in from_json to_json
|
| 1795 |
+
expected = DataFrame({"a": [1, 2], "b": [3, 4]})
|
| 1796 |
+
dfjson = expected.to_json(orient=orient, index=index)
|
| 1797 |
+
result = read_json(StringIO(dfjson), orient=orient)
|
| 1798 |
+
tm.assert_frame_equal(result, expected)
|
| 1799 |
+
|
| 1800 |
+
def test_read_timezone_information(self):
|
| 1801 |
+
# GH 25546
|
| 1802 |
+
result = read_json(
|
| 1803 |
+
StringIO('{"2019-01-01T11:00:00.000Z":88}'), typ="series", orient="index"
|
| 1804 |
+
)
|
| 1805 |
+
exp_dti = DatetimeIndex(["2019-01-01 11:00:00"], dtype="M8[us, UTC]")
|
| 1806 |
+
expected = Series([88], index=exp_dti)
|
| 1807 |
+
tm.assert_series_equal(result, expected)
|
| 1808 |
+
|
| 1809 |
+
@pytest.mark.parametrize(
|
| 1810 |
+
"url",
|
| 1811 |
+
[
|
| 1812 |
+
"s3://example-fsspec/",
|
| 1813 |
+
"gcs://another-fsspec/file.json",
|
| 1814 |
+
"filecache::s3://yet-another-fsspec/file.json",
|
| 1815 |
+
"https://example-site.com/data",
|
| 1816 |
+
"some-protocol://data.txt",
|
| 1817 |
+
],
|
| 1818 |
+
)
|
| 1819 |
+
def test_read_json_with_url_value(self, url):
|
| 1820 |
+
# GH 36271
|
| 1821 |
+
result = read_json(StringIO(f'{{"url":{{"0":"{url}"}}}}'))
|
| 1822 |
+
expected = DataFrame({"url": [url]})
|
| 1823 |
+
tm.assert_frame_equal(result, expected)
|
| 1824 |
+
|
| 1825 |
+
@pytest.mark.parametrize(
|
| 1826 |
+
"compression",
|
| 1827 |
+
["", ".gz", ".bz2", ".tar"],
|
| 1828 |
+
)
|
| 1829 |
+
def test_read_json_with_very_long_file_path(self, compression):
|
| 1830 |
+
# GH 46718
|
| 1831 |
+
long_json_path = f"{'a' * 1000}.json{compression}"
|
| 1832 |
+
with pytest.raises(
|
| 1833 |
+
FileNotFoundError, match=f"File {long_json_path} does not exist"
|
| 1834 |
+
):
|
| 1835 |
+
# path too long for Windows is handled in file_exists() but raises in
|
| 1836 |
+
# _get_data_from_filepath()
|
| 1837 |
+
read_json(long_json_path)
|
| 1838 |
+
|
| 1839 |
+
@pytest.mark.parametrize(
|
| 1840 |
+
"date_format,key", [("epoch", 86400000), ("iso", "1970-01-02T00:00:00.000")]
|
| 1841 |
+
)
|
| 1842 |
+
def test_datetime_as_label(self, date_format, key, unit):
|
| 1843 |
+
df = DataFrame(
|
| 1844 |
+
[[1]], columns=[(Timestamp(0) + pd.Timedelta("1D")).as_unit(unit)]
|
| 1845 |
+
)
|
| 1846 |
+
expected = f'{{"{key}":{{"0":1}}}}'
|
| 1847 |
+
|
| 1848 |
+
expected_warning = None
|
| 1849 |
+
if date_format == "epoch":
|
| 1850 |
+
expected_warning = Pandas4Warning
|
| 1851 |
+
|
| 1852 |
+
msg = (
|
| 1853 |
+
"'epoch' date format is deprecated and will be removed in a future "
|
| 1854 |
+
"version, please use 'iso' date format instead."
|
| 1855 |
+
)
|
| 1856 |
+
with tm.assert_produces_warning(expected_warning, match=msg):
|
| 1857 |
+
result = df.to_json(date_format=date_format)
|
| 1858 |
+
|
| 1859 |
+
assert result == expected
|
| 1860 |
+
|
| 1861 |
+
@pytest.mark.parametrize(
|
| 1862 |
+
"date_format,key", [("epoch", 86400000), ("iso", "P1DT0H0M0S")]
|
| 1863 |
+
)
|
| 1864 |
+
def test_timedelta_as_label(self, date_format, key, unit):
|
| 1865 |
+
df = DataFrame([[1]], columns=[pd.Timedelta("1D").as_unit(unit)])
|
| 1866 |
+
expected = f'{{"{key}":{{"0":1}}}}'
|
| 1867 |
+
|
| 1868 |
+
expected_warning = None
|
| 1869 |
+
if date_format == "epoch":
|
| 1870 |
+
expected_warning = Pandas4Warning
|
| 1871 |
+
|
| 1872 |
+
msg = (
|
| 1873 |
+
"'epoch' date format is deprecated and will be removed in a future "
|
| 1874 |
+
"version, please use 'iso' date format instead."
|
| 1875 |
+
)
|
| 1876 |
+
with tm.assert_produces_warning(expected_warning, match=msg):
|
| 1877 |
+
result = df.to_json(date_format=date_format)
|
| 1878 |
+
|
| 1879 |
+
assert result == expected
|
| 1880 |
+
|
| 1881 |
+
@pytest.mark.parametrize(
|
| 1882 |
+
"orient,expected",
|
| 1883 |
+
[
|
| 1884 |
+
("index", "{\"('a', 'b')\":{\"('c', 'd')\":1}}"),
|
| 1885 |
+
("columns", "{\"('c', 'd')\":{\"('a', 'b')\":1}}"),
|
| 1886 |
+
# TODO: the below have separate encoding procedures
|
| 1887 |
+
pytest.param(
|
| 1888 |
+
"split",
|
| 1889 |
+
"",
|
| 1890 |
+
marks=pytest.mark.xfail(
|
| 1891 |
+
reason="Produces JSON but not in a consistent manner"
|
| 1892 |
+
),
|
| 1893 |
+
),
|
| 1894 |
+
pytest.param(
|
| 1895 |
+
"table",
|
| 1896 |
+
"",
|
| 1897 |
+
marks=pytest.mark.xfail(
|
| 1898 |
+
reason="Produces JSON but not in a consistent manner"
|
| 1899 |
+
),
|
| 1900 |
+
),
|
| 1901 |
+
],
|
| 1902 |
+
)
|
| 1903 |
+
def test_tuple_labels(self, orient, expected):
|
| 1904 |
+
# GH 20500
|
| 1905 |
+
df = DataFrame([[1]], index=[("a", "b")], columns=[("c", "d")])
|
| 1906 |
+
result = df.to_json(orient=orient)
|
| 1907 |
+
assert result == expected
|
| 1908 |
+
|
| 1909 |
+
@pytest.mark.parametrize("indent", [1, 2, 4])
|
| 1910 |
+
def test_to_json_indent(self, indent):
|
| 1911 |
+
# GH 12004
|
| 1912 |
+
df = DataFrame([["foo", "bar"], ["baz", "qux"]], columns=["a", "b"])
|
| 1913 |
+
|
| 1914 |
+
result = df.to_json(indent=indent)
|
| 1915 |
+
spaces = " " * indent
|
| 1916 |
+
expected = f"""{{
|
| 1917 |
+
{spaces}"a":{{
|
| 1918 |
+
{spaces}{spaces}"0":"foo",
|
| 1919 |
+
{spaces}{spaces}"1":"baz"
|
| 1920 |
+
{spaces}}},
|
| 1921 |
+
{spaces}"b":{{
|
| 1922 |
+
{spaces}{spaces}"0":"bar",
|
| 1923 |
+
{spaces}{spaces}"1":"qux"
|
| 1924 |
+
{spaces}}}
|
| 1925 |
+
}}"""
|
| 1926 |
+
|
| 1927 |
+
assert result == expected
|
| 1928 |
+
|
| 1929 |
+
@pytest.mark.skipif(
|
| 1930 |
+
using_string_dtype(),
|
| 1931 |
+
reason="Adjust expected when infer_string is default, no bug here, "
|
| 1932 |
+
"just a complicated parametrization",
|
| 1933 |
+
)
|
| 1934 |
+
@pytest.mark.parametrize(
|
| 1935 |
+
"orient,expected",
|
| 1936 |
+
[
|
| 1937 |
+
(
|
| 1938 |
+
"split",
|
| 1939 |
+
"""{
|
| 1940 |
+
"columns":[
|
| 1941 |
+
"a",
|
| 1942 |
+
"b"
|
| 1943 |
+
],
|
| 1944 |
+
"index":[
|
| 1945 |
+
0,
|
| 1946 |
+
1
|
| 1947 |
+
],
|
| 1948 |
+
"data":[
|
| 1949 |
+
[
|
| 1950 |
+
"foo",
|
| 1951 |
+
"bar"
|
| 1952 |
+
],
|
| 1953 |
+
[
|
| 1954 |
+
"baz",
|
| 1955 |
+
"qux"
|
| 1956 |
+
]
|
| 1957 |
+
]
|
| 1958 |
+
}""",
|
| 1959 |
+
),
|
| 1960 |
+
(
|
| 1961 |
+
"records",
|
| 1962 |
+
"""[
|
| 1963 |
+
{
|
| 1964 |
+
"a":"foo",
|
| 1965 |
+
"b":"bar"
|
| 1966 |
+
},
|
| 1967 |
+
{
|
| 1968 |
+
"a":"baz",
|
| 1969 |
+
"b":"qux"
|
| 1970 |
+
}
|
| 1971 |
+
]""",
|
| 1972 |
+
),
|
| 1973 |
+
(
|
| 1974 |
+
"index",
|
| 1975 |
+
"""{
|
| 1976 |
+
"0":{
|
| 1977 |
+
"a":"foo",
|
| 1978 |
+
"b":"bar"
|
| 1979 |
+
},
|
| 1980 |
+
"1":{
|
| 1981 |
+
"a":"baz",
|
| 1982 |
+
"b":"qux"
|
| 1983 |
+
}
|
| 1984 |
+
}""",
|
| 1985 |
+
),
|
| 1986 |
+
(
|
| 1987 |
+
"columns",
|
| 1988 |
+
"""{
|
| 1989 |
+
"a":{
|
| 1990 |
+
"0":"foo",
|
| 1991 |
+
"1":"baz"
|
| 1992 |
+
},
|
| 1993 |
+
"b":{
|
| 1994 |
+
"0":"bar",
|
| 1995 |
+
"1":"qux"
|
| 1996 |
+
}
|
| 1997 |
+
}""",
|
| 1998 |
+
),
|
| 1999 |
+
(
|
| 2000 |
+
"values",
|
| 2001 |
+
"""[
|
| 2002 |
+
[
|
| 2003 |
+
"foo",
|
| 2004 |
+
"bar"
|
| 2005 |
+
],
|
| 2006 |
+
[
|
| 2007 |
+
"baz",
|
| 2008 |
+
"qux"
|
| 2009 |
+
]
|
| 2010 |
+
]""",
|
| 2011 |
+
),
|
| 2012 |
+
(
|
| 2013 |
+
"table",
|
| 2014 |
+
"""{
|
| 2015 |
+
"schema":{
|
| 2016 |
+
"fields":[
|
| 2017 |
+
{
|
| 2018 |
+
"name":"index",
|
| 2019 |
+
"type":"integer"
|
| 2020 |
+
},
|
| 2021 |
+
{
|
| 2022 |
+
"name":"a",
|
| 2023 |
+
"type":"string"
|
| 2024 |
+
},
|
| 2025 |
+
{
|
| 2026 |
+
"name":"b",
|
| 2027 |
+
"type":"string"
|
| 2028 |
+
}
|
| 2029 |
+
],
|
| 2030 |
+
"primaryKey":[
|
| 2031 |
+
"index"
|
| 2032 |
+
],
|
| 2033 |
+
"pandas_version":"1.4.0"
|
| 2034 |
+
},
|
| 2035 |
+
"data":[
|
| 2036 |
+
{
|
| 2037 |
+
"index":0,
|
| 2038 |
+
"a":"foo",
|
| 2039 |
+
"b":"bar"
|
| 2040 |
+
},
|
| 2041 |
+
{
|
| 2042 |
+
"index":1,
|
| 2043 |
+
"a":"baz",
|
| 2044 |
+
"b":"qux"
|
| 2045 |
+
}
|
| 2046 |
+
]
|
| 2047 |
+
}""",
|
| 2048 |
+
),
|
| 2049 |
+
],
|
| 2050 |
+
)
|
| 2051 |
+
def test_json_indent_all_orients(self, orient, expected):
|
| 2052 |
+
# GH 12004
|
| 2053 |
+
df = DataFrame([["foo", "bar"], ["baz", "qux"]], columns=["a", "b"])
|
| 2054 |
+
result = df.to_json(orient=orient, indent=4)
|
| 2055 |
+
assert result == expected
|
| 2056 |
+
|
| 2057 |
+
def test_json_negative_indent_raises(self):
|
| 2058 |
+
with pytest.raises(ValueError, match="must be a nonnegative integer"):
|
| 2059 |
+
DataFrame().to_json(indent=-1)
|
| 2060 |
+
|
| 2061 |
+
def test_emca_262_nan_inf_support(self):
|
| 2062 |
+
# GH 12213
|
| 2063 |
+
data = StringIO(
|
| 2064 |
+
'["a", NaN, "NaN", Infinity, "Infinity", -Infinity, "-Infinity"]'
|
| 2065 |
+
)
|
| 2066 |
+
result = read_json(data)
|
| 2067 |
+
expected = DataFrame(
|
| 2068 |
+
["a", None, "NaN", np.inf, "Infinity", -np.inf, "-Infinity"]
|
| 2069 |
+
)
|
| 2070 |
+
tm.assert_frame_equal(result, expected)
|
| 2071 |
+
|
| 2072 |
+
def test_frame_int_overflow(self):
|
| 2073 |
+
# GH 30320
|
| 2074 |
+
encoded_json = json.dumps([{"col": "31900441201190696999"}, {"col": "Text"}])
|
| 2075 |
+
expected = DataFrame({"col": ["31900441201190696999", "Text"]})
|
| 2076 |
+
result = read_json(StringIO(encoded_json))
|
| 2077 |
+
tm.assert_frame_equal(result, expected)
|
| 2078 |
+
|
| 2079 |
+
def test_json_multiindex(self):
|
| 2080 |
+
dataframe = DataFrame({"x": [1, 2, 3], "y": ["a", "b", "c"]})
|
| 2081 |
+
expected = (
|
| 2082 |
+
'{"(0, \'x\')":1,"(0, \'y\')":"a","(1, \'x\')":2,'
|
| 2083 |
+
'"(1, \'y\')":"b","(2, \'x\')":3,"(2, \'y\')":"c"}'
|
| 2084 |
+
)
|
| 2085 |
+
series = dataframe.stack()
|
| 2086 |
+
result = series.to_json(orient="index")
|
| 2087 |
+
assert result == expected
|
| 2088 |
+
|
| 2089 |
+
@pytest.mark.single_cpu
|
| 2090 |
+
@pytest.mark.network
|
| 2091 |
+
def test_to_s3(self, s3_bucket_public, s3so):
|
| 2092 |
+
# GH 28375
|
| 2093 |
+
mock_bucket_name = s3_bucket_public.name
|
| 2094 |
+
target_file = f"{uuid.uuid4()}.json"
|
| 2095 |
+
df = DataFrame({"x": [1, 2, 3], "y": [2, 4, 6]})
|
| 2096 |
+
uri = f"s3://{mock_bucket_name}/{target_file}"
|
| 2097 |
+
df.to_json(uri, storage_options=s3so)
|
| 2098 |
+
result = read_json(uri, storage_options=s3so)
|
| 2099 |
+
tm.assert_frame_equal(result, df)
|
| 2100 |
+
|
| 2101 |
+
def test_json_pandas_nulls(self, nulls_fixture):
|
| 2102 |
+
# GH 31615
|
| 2103 |
+
expected_warning = None
|
| 2104 |
+
msg = (
|
| 2105 |
+
"The default 'epoch' date format is deprecated and will be removed "
|
| 2106 |
+
"in a future version, please use 'iso' date format instead."
|
| 2107 |
+
)
|
| 2108 |
+
if nulls_fixture is pd.NaT:
|
| 2109 |
+
expected_warning = Pandas4Warning
|
| 2110 |
+
|
| 2111 |
+
with tm.assert_produces_warning(expected_warning, match=msg):
|
| 2112 |
+
result = DataFrame([[nulls_fixture]]).to_json()
|
| 2113 |
+
assert result == '{"0":{"0":null}}'
|
| 2114 |
+
|
| 2115 |
+
def test_readjson_bool_series(self):
|
| 2116 |
+
# GH31464
|
| 2117 |
+
result = read_json(StringIO("[true, true, false]"), typ="series")
|
| 2118 |
+
expected = Series([True, True, False])
|
| 2119 |
+
tm.assert_series_equal(result, expected)
|
| 2120 |
+
|
| 2121 |
+
def test_to_json_multiindex_escape(self):
|
| 2122 |
+
# GH 15273
|
| 2123 |
+
df = DataFrame(
|
| 2124 |
+
True,
|
| 2125 |
+
index=date_range("2017-01-20", "2017-01-23", unit="ns"),
|
| 2126 |
+
columns=["foo", "bar"],
|
| 2127 |
+
).stack()
|
| 2128 |
+
result = df.to_json()
|
| 2129 |
+
expected = (
|
| 2130 |
+
"{\"(Timestamp('2017-01-20 00:00:00'), 'foo')\":true,"
|
| 2131 |
+
"\"(Timestamp('2017-01-20 00:00:00'), 'bar')\":true,"
|
| 2132 |
+
"\"(Timestamp('2017-01-21 00:00:00'), 'foo')\":true,"
|
| 2133 |
+
"\"(Timestamp('2017-01-21 00:00:00'), 'bar')\":true,"
|
| 2134 |
+
"\"(Timestamp('2017-01-22 00:00:00'), 'foo')\":true,"
|
| 2135 |
+
"\"(Timestamp('2017-01-22 00:00:00'), 'bar')\":true,"
|
| 2136 |
+
"\"(Timestamp('2017-01-23 00:00:00'), 'foo')\":true,"
|
| 2137 |
+
"\"(Timestamp('2017-01-23 00:00:00'), 'bar')\":true}"
|
| 2138 |
+
)
|
| 2139 |
+
assert result == expected
|
| 2140 |
+
|
| 2141 |
+
def test_to_json_series_of_objects(self):
|
| 2142 |
+
class _TestObject:
|
| 2143 |
+
def __init__(self, a, b, _c, d) -> None:
|
| 2144 |
+
self.a = a
|
| 2145 |
+
self.b = b
|
| 2146 |
+
self._c = _c
|
| 2147 |
+
self.d = d
|
| 2148 |
+
|
| 2149 |
+
def e(self):
|
| 2150 |
+
return 5
|
| 2151 |
+
|
| 2152 |
+
# JSON keys should be all non-callable non-underscore attributes, see GH-42768
|
| 2153 |
+
series = Series([_TestObject(a=1, b=2, _c=3, d=4)])
|
| 2154 |
+
assert json.loads(series.to_json()) == {"0": {"a": 1, "b": 2, "d": 4}}
|
| 2155 |
+
|
| 2156 |
+
@pytest.mark.parametrize(
|
| 2157 |
+
"data,expected",
|
| 2158 |
+
[
|
| 2159 |
+
(
|
| 2160 |
+
Series({0: -6 + 8j, 1: 0 + 1j, 2: 9 - 5j}),
|
| 2161 |
+
'{"0":{"imag":8.0,"real":-6.0},'
|
| 2162 |
+
'"1":{"imag":1.0,"real":0.0},'
|
| 2163 |
+
'"2":{"imag":-5.0,"real":9.0}}',
|
| 2164 |
+
),
|
| 2165 |
+
(
|
| 2166 |
+
Series({0: -9.39 + 0.66j, 1: 3.95 + 9.32j, 2: 4.03 - 0.17j}),
|
| 2167 |
+
'{"0":{"imag":0.66,"real":-9.39},'
|
| 2168 |
+
'"1":{"imag":9.32,"real":3.95},'
|
| 2169 |
+
'"2":{"imag":-0.17,"real":4.03}}',
|
| 2170 |
+
),
|
| 2171 |
+
(
|
| 2172 |
+
DataFrame([[-2 + 3j, -1 - 0j], [4 - 3j, -0 - 10j]]),
|
| 2173 |
+
'{"0":{"0":{"imag":3.0,"real":-2.0},'
|
| 2174 |
+
'"1":{"imag":-3.0,"real":4.0}},'
|
| 2175 |
+
'"1":{"0":{"imag":0.0,"real":-1.0},'
|
| 2176 |
+
'"1":{"imag":-10.0,"real":0.0}}}',
|
| 2177 |
+
),
|
| 2178 |
+
(
|
| 2179 |
+
DataFrame(
|
| 2180 |
+
[[-0.28 + 0.34j, -1.08 - 0.39j], [0.41 - 0.34j, -0.78 - 1.35j]]
|
| 2181 |
+
),
|
| 2182 |
+
'{"0":{"0":{"imag":0.34,"real":-0.28},'
|
| 2183 |
+
'"1":{"imag":-0.34,"real":0.41}},'
|
| 2184 |
+
'"1":{"0":{"imag":-0.39,"real":-1.08},'
|
| 2185 |
+
'"1":{"imag":-1.35,"real":-0.78}}}',
|
| 2186 |
+
),
|
| 2187 |
+
],
|
| 2188 |
+
)
|
| 2189 |
+
def test_complex_data_tojson(self, data, expected):
|
| 2190 |
+
# GH41174
|
| 2191 |
+
result = data.to_json()
|
| 2192 |
+
assert result == expected
|
| 2193 |
+
|
| 2194 |
+
def test_json_uint64(self):
|
| 2195 |
+
# GH21073
|
| 2196 |
+
expected = (
|
| 2197 |
+
'{"columns":["col1"],"index":[0,1],'
|
| 2198 |
+
'"data":[[13342205958987758245],[12388075603347835679]]}'
|
| 2199 |
+
)
|
| 2200 |
+
df = DataFrame(data={"col1": [13342205958987758245, 12388075603347835679]})
|
| 2201 |
+
result = df.to_json(orient="split")
|
| 2202 |
+
assert result == expected
|
| 2203 |
+
|
| 2204 |
+
def test_read_json_dtype_backend(
|
| 2205 |
+
self, string_storage, dtype_backend, orient, using_infer_string
|
| 2206 |
+
):
|
| 2207 |
+
# GH#50750
|
| 2208 |
+
df = DataFrame(
|
| 2209 |
+
{
|
| 2210 |
+
"a": Series([1, NA, 3], dtype="Int64"),
|
| 2211 |
+
"b": Series([1, 2, 3], dtype="Int64"),
|
| 2212 |
+
"c": Series([1.5, NA, 2.5], dtype="Float64"),
|
| 2213 |
+
"d": Series([1.5, 2.0, 2.5], dtype="Float64"),
|
| 2214 |
+
"e": [True, False, None],
|
| 2215 |
+
"f": [True, False, True],
|
| 2216 |
+
"g": ["a", "b", "c"],
|
| 2217 |
+
"h": ["a", "b", None],
|
| 2218 |
+
}
|
| 2219 |
+
)
|
| 2220 |
+
|
| 2221 |
+
out = df.to_json(orient=orient)
|
| 2222 |
+
with pd.option_context("mode.string_storage", string_storage):
|
| 2223 |
+
result = read_json(
|
| 2224 |
+
StringIO(out), dtype_backend=dtype_backend, orient=orient
|
| 2225 |
+
)
|
| 2226 |
+
|
| 2227 |
+
if dtype_backend == "pyarrow":
|
| 2228 |
+
pa = pytest.importorskip("pyarrow")
|
| 2229 |
+
string_dtype = pd.ArrowDtype(pa.string())
|
| 2230 |
+
else:
|
| 2231 |
+
string_dtype = pd.StringDtype(string_storage)
|
| 2232 |
+
|
| 2233 |
+
expected = DataFrame(
|
| 2234 |
+
{
|
| 2235 |
+
"a": Series([1, NA, 3], dtype="Int64"),
|
| 2236 |
+
"b": Series([1, 2, 3], dtype="Int64"),
|
| 2237 |
+
"c": Series([1.5, NA, 2.5], dtype="Float64"),
|
| 2238 |
+
"d": Series([1.5, 2.0, 2.5], dtype="Float64"),
|
| 2239 |
+
"e": Series([True, False, NA], dtype="boolean"),
|
| 2240 |
+
"f": Series([True, False, True], dtype="boolean"),
|
| 2241 |
+
"g": Series(["a", "b", "c"], dtype=string_dtype),
|
| 2242 |
+
"h": Series(["a", "b", None], dtype=string_dtype),
|
| 2243 |
+
}
|
| 2244 |
+
)
|
| 2245 |
+
|
| 2246 |
+
if dtype_backend == "pyarrow":
|
| 2247 |
+
pa = pytest.importorskip("pyarrow")
|
| 2248 |
+
from pandas.arrays import ArrowExtensionArray
|
| 2249 |
+
|
| 2250 |
+
expected = DataFrame(
|
| 2251 |
+
{
|
| 2252 |
+
col: ArrowExtensionArray(pa.array(expected[col], from_pandas=True))
|
| 2253 |
+
for col in expected.columns
|
| 2254 |
+
}
|
| 2255 |
+
)
|
| 2256 |
+
|
| 2257 |
+
if orient == "values":
|
| 2258 |
+
expected.columns = list(range(8))
|
| 2259 |
+
|
| 2260 |
+
# the storage of the str columns' Index is also affected by the
|
| 2261 |
+
# string_storage setting -> ignore that for checking the result
|
| 2262 |
+
tm.assert_frame_equal(result, expected, check_column_type=False)
|
| 2263 |
+
|
| 2264 |
+
@td.skip_if_no("pyarrow")
|
| 2265 |
+
@pytest.mark.filterwarnings("ignore:Passing a BlockManager:DeprecationWarning")
|
| 2266 |
+
def test_read_json_pyarrow_with_dtype(self):
|
| 2267 |
+
dtype = {"a": "int32[pyarrow]", "b": "int64[pyarrow]"}
|
| 2268 |
+
json = b'{"a": 1, "b": 2}\n'
|
| 2269 |
+
|
| 2270 |
+
df = read_json(
|
| 2271 |
+
BytesIO(json),
|
| 2272 |
+
dtype=dtype,
|
| 2273 |
+
lines=True,
|
| 2274 |
+
engine="pyarrow",
|
| 2275 |
+
dtype_backend="pyarrow",
|
| 2276 |
+
)
|
| 2277 |
+
|
| 2278 |
+
result = df.dtypes
|
| 2279 |
+
expected = Series(
|
| 2280 |
+
data=[
|
| 2281 |
+
pd.ArrowDtype.construct_from_string("int32[pyarrow]"),
|
| 2282 |
+
pd.ArrowDtype.construct_from_string("int64[pyarrow]"),
|
| 2283 |
+
],
|
| 2284 |
+
index=["a", "b"],
|
| 2285 |
+
)
|
| 2286 |
+
tm.assert_series_equal(result, expected)
|
| 2287 |
+
|
| 2288 |
+
@pytest.mark.parametrize("orient", ["split", "records", "index"])
|
| 2289 |
+
def test_read_json_nullable_series(self, string_storage, dtype_backend, orient):
|
| 2290 |
+
# GH#50750
|
| 2291 |
+
pa = pytest.importorskip("pyarrow")
|
| 2292 |
+
ser = Series([1, NA, 3], dtype="Int64")
|
| 2293 |
+
|
| 2294 |
+
out = ser.to_json(orient=orient)
|
| 2295 |
+
with pd.option_context("mode.string_storage", string_storage):
|
| 2296 |
+
result = read_json(
|
| 2297 |
+
StringIO(out), dtype_backend=dtype_backend, orient=orient, typ="series"
|
| 2298 |
+
)
|
| 2299 |
+
|
| 2300 |
+
expected = Series([1, NA, 3], dtype="Int64")
|
| 2301 |
+
|
| 2302 |
+
if dtype_backend == "pyarrow":
|
| 2303 |
+
from pandas.arrays import ArrowExtensionArray
|
| 2304 |
+
|
| 2305 |
+
expected = Series(ArrowExtensionArray(pa.array(expected, from_pandas=True)))
|
| 2306 |
+
|
| 2307 |
+
tm.assert_series_equal(result, expected)
|
| 2308 |
+
|
| 2309 |
+
def test_invalid_dtype_backend(self):
|
| 2310 |
+
msg = (
|
| 2311 |
+
"dtype_backend numpy is invalid, only 'numpy_nullable' and "
|
| 2312 |
+
"'pyarrow' are allowed."
|
| 2313 |
+
)
|
| 2314 |
+
with pytest.raises(ValueError, match=msg):
|
| 2315 |
+
read_json("test", dtype_backend="numpy")
|
| 2316 |
+
|
| 2317 |
+
|
| 2318 |
+
def test_invalid_engine():
|
| 2319 |
+
# GH 48893
|
| 2320 |
+
ser = Series(range(1))
|
| 2321 |
+
out = ser.to_json()
|
| 2322 |
+
with pytest.raises(ValueError, match="The engine type foo"):
|
| 2323 |
+
read_json(out, engine="foo")
|
| 2324 |
+
|
| 2325 |
+
|
| 2326 |
+
def test_pyarrow_engine_lines_false():
|
| 2327 |
+
# GH 48893
|
| 2328 |
+
ser = Series(range(1))
|
| 2329 |
+
out = ser.to_json()
|
| 2330 |
+
with pytest.raises(ValueError, match="currently pyarrow engine only supports"):
|
| 2331 |
+
read_json(out, engine="pyarrow", lines=False)
|
| 2332 |
+
|
| 2333 |
+
|
| 2334 |
+
def test_json_roundtrip_string_inference(orient):
|
| 2335 |
+
df = DataFrame(
|
| 2336 |
+
[["a", "b"], ["c", "d"]], index=["row 1", "row 2"], columns=["col 1", "col 2"]
|
| 2337 |
+
)
|
| 2338 |
+
out = df.to_json()
|
| 2339 |
+
with pd.option_context("future.infer_string", True):
|
| 2340 |
+
result = read_json(StringIO(out))
|
| 2341 |
+
dtype = pd.StringDtype(na_value=np.nan)
|
| 2342 |
+
expected = DataFrame(
|
| 2343 |
+
[["a", "b"], ["c", "d"]],
|
| 2344 |
+
dtype=dtype,
|
| 2345 |
+
index=Index(["row 1", "row 2"], dtype=dtype),
|
| 2346 |
+
columns=Index(["col 1", "col 2"], dtype=dtype),
|
| 2347 |
+
)
|
| 2348 |
+
tm.assert_frame_equal(result, expected)
|
| 2349 |
+
|
| 2350 |
+
|
| 2351 |
+
@td.skip_if_no("pyarrow")
|
| 2352 |
+
def test_to_json_ea_null():
|
| 2353 |
+
# GH#57224
|
| 2354 |
+
df = DataFrame(
|
| 2355 |
+
{
|
| 2356 |
+
"a": Series([1, NA], dtype="int64[pyarrow]"),
|
| 2357 |
+
"b": Series([2, NA], dtype="Int64"),
|
| 2358 |
+
}
|
| 2359 |
+
)
|
| 2360 |
+
result = df.to_json(orient="records", lines=True)
|
| 2361 |
+
expected = """{"a":1,"b":2}
|
| 2362 |
+
{"a":null,"b":null}
|
| 2363 |
+
"""
|
| 2364 |
+
assert result == expected
|
| 2365 |
+
|
| 2366 |
+
|
| 2367 |
+
def test_read_json_lines_rangeindex():
|
| 2368 |
+
# GH 57429
|
| 2369 |
+
data = """
|
| 2370 |
+
{"a": 1, "b": 2}
|
| 2371 |
+
{"a": 3, "b": 4}
|
| 2372 |
+
"""
|
| 2373 |
+
result = read_json(StringIO(data), lines=True).index
|
| 2374 |
+
expected = RangeIndex(2)
|
| 2375 |
+
tm.assert_index_equal(result, expected, exact=True)
|
| 2376 |
+
|
| 2377 |
+
|
| 2378 |
+
def test_large_number():
|
| 2379 |
+
# GH#20608
|
| 2380 |
+
result = read_json(
|
| 2381 |
+
StringIO('["9999999999999999"]'),
|
| 2382 |
+
orient="values",
|
| 2383 |
+
typ="series",
|
| 2384 |
+
convert_dates=False,
|
| 2385 |
+
)
|
| 2386 |
+
expected = Series([9999999999999999])
|
| 2387 |
+
tm.assert_series_equal(result, expected)
|
.venv/lib/python3.13/site-packages/pandas/tests/io/json/test_readlines.py
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from collections.abc import Iterator
|
| 2 |
+
from io import (
|
| 3 |
+
BytesIO,
|
| 4 |
+
StringIO,
|
| 5 |
+
)
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import pytest
|
| 10 |
+
|
| 11 |
+
import pandas as pd
|
| 12 |
+
from pandas import (
|
| 13 |
+
DataFrame,
|
| 14 |
+
read_json,
|
| 15 |
+
)
|
| 16 |
+
import pandas._testing as tm
|
| 17 |
+
|
| 18 |
+
from pandas.io.json._json import JsonReader
|
| 19 |
+
|
| 20 |
+
pytestmark = pytest.mark.filterwarnings(
|
| 21 |
+
"ignore:Passing a BlockManager to DataFrame:DeprecationWarning"
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@pytest.fixture
|
| 26 |
+
def lines_json_df():
|
| 27 |
+
df = DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
|
| 28 |
+
return df.to_json(lines=True, orient="records")
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@pytest.fixture(params=["ujson", "pyarrow"])
|
| 32 |
+
def engine(request):
|
| 33 |
+
if request.param == "pyarrow":
|
| 34 |
+
pytest.importorskip("pyarrow.json")
|
| 35 |
+
return request.param
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def test_read_jsonl():
|
| 39 |
+
# GH9180
|
| 40 |
+
result = read_json(StringIO('{"a": 1, "b": 2}\n{"b":2, "a" :1}\n'), lines=True)
|
| 41 |
+
expected = DataFrame([[1, 2], [1, 2]], columns=["a", "b"])
|
| 42 |
+
tm.assert_frame_equal(result, expected)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def test_read_jsonl_engine_pyarrow(datapath, engine):
|
| 46 |
+
result = read_json(
|
| 47 |
+
datapath("io", "json", "data", "line_delimited.json"),
|
| 48 |
+
lines=True,
|
| 49 |
+
engine=engine,
|
| 50 |
+
)
|
| 51 |
+
expected = DataFrame({"a": [1, 3, 5], "b": [2, 4, 6]})
|
| 52 |
+
tm.assert_frame_equal(result, expected)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def test_read_datetime(request, engine):
|
| 56 |
+
# GH33787
|
| 57 |
+
if engine == "pyarrow":
|
| 58 |
+
# GH 48893
|
| 59 |
+
reason = "Pyarrow only supports a file path as an input and line delimited json"
|
| 60 |
+
request.applymarker(pytest.mark.xfail(reason=reason, raises=ValueError))
|
| 61 |
+
|
| 62 |
+
df = DataFrame(
|
| 63 |
+
[([1, 2], ["2020-03-05", "2020-04-08T09:58:49+00:00"], "hector")],
|
| 64 |
+
columns=["accounts", "date", "name"],
|
| 65 |
+
)
|
| 66 |
+
json_line = df.to_json(lines=True, orient="records")
|
| 67 |
+
|
| 68 |
+
if engine == "pyarrow":
|
| 69 |
+
result = read_json(StringIO(json_line), engine=engine)
|
| 70 |
+
else:
|
| 71 |
+
result = read_json(StringIO(json_line), engine=engine)
|
| 72 |
+
expected = DataFrame(
|
| 73 |
+
[[1, "2020-03-05", "hector"], [2, "2020-04-08T09:58:49+00:00", "hector"]],
|
| 74 |
+
columns=["accounts", "date", "name"],
|
| 75 |
+
)
|
| 76 |
+
tm.assert_frame_equal(result, expected)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def test_read_jsonl_unicode_chars():
|
| 80 |
+
# GH15132: non-ascii unicode characters
|
| 81 |
+
# \u201d == RIGHT DOUBLE QUOTATION MARK
|
| 82 |
+
|
| 83 |
+
# simulate file handle
|
| 84 |
+
json = '{"a": "foo”", "b": "bar"}\n{"a": "foo", "b": "bar"}\n'
|
| 85 |
+
json = StringIO(json)
|
| 86 |
+
result = read_json(json, lines=True)
|
| 87 |
+
expected = DataFrame([["foo\u201d", "bar"], ["foo", "bar"]], columns=["a", "b"])
|
| 88 |
+
tm.assert_frame_equal(result, expected)
|
| 89 |
+
|
| 90 |
+
# simulate string
|
| 91 |
+
json = '{"a": "foo”", "b": "bar"}\n{"a": "foo", "b": "bar"}\n'
|
| 92 |
+
result = read_json(StringIO(json), lines=True)
|
| 93 |
+
expected = DataFrame([["foo\u201d", "bar"], ["foo", "bar"]], columns=["a", "b"])
|
| 94 |
+
tm.assert_frame_equal(result, expected)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def test_to_jsonl():
|
| 98 |
+
# GH9180
|
| 99 |
+
df = DataFrame([[1, 2], [1, 2]], columns=["a", "b"])
|
| 100 |
+
result = df.to_json(orient="records", lines=True)
|
| 101 |
+
expected = '{"a":1,"b":2}\n{"a":1,"b":2}\n'
|
| 102 |
+
assert result == expected
|
| 103 |
+
|
| 104 |
+
df = DataFrame([["foo}", "bar"], ['foo"', "bar"]], columns=["a", "b"])
|
| 105 |
+
result = df.to_json(orient="records", lines=True)
|
| 106 |
+
expected = '{"a":"foo}","b":"bar"}\n{"a":"foo\\"","b":"bar"}\n'
|
| 107 |
+
assert result == expected
|
| 108 |
+
tm.assert_frame_equal(read_json(StringIO(result), lines=True), df)
|
| 109 |
+
|
| 110 |
+
# GH15096: escaped characters in columns and data
|
| 111 |
+
df = DataFrame([["foo\\", "bar"], ['foo"', "bar"]], columns=["a\\", "b"])
|
| 112 |
+
result = df.to_json(orient="records", lines=True)
|
| 113 |
+
expected = '{"a\\\\":"foo\\\\","b":"bar"}\n{"a\\\\":"foo\\"","b":"bar"}\n'
|
| 114 |
+
assert result == expected
|
| 115 |
+
tm.assert_frame_equal(read_json(StringIO(result), lines=True), df)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def test_to_jsonl_count_new_lines():
|
| 119 |
+
# GH36888
|
| 120 |
+
df = DataFrame([[1, 2], [1, 2]], columns=["a", "b"])
|
| 121 |
+
actual_new_lines_count = df.to_json(orient="records", lines=True).count("\n")
|
| 122 |
+
expected_new_lines_count = 2
|
| 123 |
+
assert actual_new_lines_count == expected_new_lines_count
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
@pytest.mark.parametrize("chunksize", [1, 1.0])
|
| 127 |
+
@pytest.mark.parametrize("buffer", [BytesIO, StringIO])
|
| 128 |
+
def test_readjson_chunks(request, lines_json_df, chunksize, buffer, engine):
|
| 129 |
+
# Basic test that read_json(chunks=True) gives the same result as
|
| 130 |
+
# read_json(chunks=False)
|
| 131 |
+
# GH17048: memory usage when lines=True
|
| 132 |
+
# GH#28906: read binary json lines in chunks
|
| 133 |
+
|
| 134 |
+
if buffer == BytesIO:
|
| 135 |
+
lines_json_df = lines_json_df.encode()
|
| 136 |
+
|
| 137 |
+
if engine == "pyarrow":
|
| 138 |
+
# GH 48893
|
| 139 |
+
reason = (
|
| 140 |
+
"Pyarrow only supports a file path as an input and line delimited json"
|
| 141 |
+
"and doesn't support chunksize parameter."
|
| 142 |
+
)
|
| 143 |
+
request.applymarker(pytest.mark.xfail(reason=reason, raises=ValueError))
|
| 144 |
+
|
| 145 |
+
unchunked = read_json(buffer(lines_json_df), lines=True)
|
| 146 |
+
with (
|
| 147 |
+
buffer(lines_json_df) as buf,
|
| 148 |
+
read_json(buf, lines=True, chunksize=chunksize, engine=engine) as reader,
|
| 149 |
+
):
|
| 150 |
+
chunked = pd.concat(reader)
|
| 151 |
+
|
| 152 |
+
tm.assert_frame_equal(chunked, unchunked)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def test_readjson_chunksize_requires_lines(lines_json_df, engine):
|
| 156 |
+
msg = "chunksize can only be passed if lines=True"
|
| 157 |
+
with pytest.raises(ValueError, match=msg):
|
| 158 |
+
with read_json(
|
| 159 |
+
StringIO(lines_json_df), lines=False, chunksize=2, engine=engine
|
| 160 |
+
) as _:
|
| 161 |
+
pass
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def test_readjson_chunks_series(request, engine):
|
| 165 |
+
if engine == "pyarrow":
|
| 166 |
+
# GH 48893
|
| 167 |
+
reason = (
|
| 168 |
+
"Pyarrow only supports a file path as an input and line delimited json"
|
| 169 |
+
"and doesn't support chunksize parameter."
|
| 170 |
+
)
|
| 171 |
+
request.applymarker(pytest.mark.xfail(reason=reason))
|
| 172 |
+
|
| 173 |
+
# Test reading line-format JSON to Series with chunksize param
|
| 174 |
+
s = pd.Series({"A": 1, "B": 2})
|
| 175 |
+
|
| 176 |
+
strio = StringIO(s.to_json(lines=True, orient="records"))
|
| 177 |
+
unchunked = read_json(strio, lines=True, typ="series", engine=engine)
|
| 178 |
+
|
| 179 |
+
strio = StringIO(s.to_json(lines=True, orient="records"))
|
| 180 |
+
with read_json(
|
| 181 |
+
strio, lines=True, typ="series", chunksize=1, engine=engine
|
| 182 |
+
) as reader:
|
| 183 |
+
chunked = pd.concat(reader)
|
| 184 |
+
|
| 185 |
+
tm.assert_series_equal(chunked, unchunked)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def test_readjson_each_chunk(request, lines_json_df, engine):
|
| 189 |
+
if engine == "pyarrow":
|
| 190 |
+
# GH 48893
|
| 191 |
+
reason = (
|
| 192 |
+
"Pyarrow only supports a file path as an input and line delimited json"
|
| 193 |
+
"and doesn't support chunksize parameter."
|
| 194 |
+
)
|
| 195 |
+
request.applymarker(pytest.mark.xfail(reason=reason, raises=ValueError))
|
| 196 |
+
|
| 197 |
+
# Other tests check that the final result of read_json(chunksize=True)
|
| 198 |
+
# is correct. This checks the intermediate chunks.
|
| 199 |
+
with read_json(
|
| 200 |
+
StringIO(lines_json_df), lines=True, chunksize=2, engine=engine
|
| 201 |
+
) as reader:
|
| 202 |
+
chunks = list(reader)
|
| 203 |
+
assert chunks[0].shape == (2, 2)
|
| 204 |
+
assert chunks[1].shape == (1, 2)
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def test_readjson_chunks_from_file(request, engine, temp_file):
|
| 208 |
+
if engine == "pyarrow":
|
| 209 |
+
# GH 48893
|
| 210 |
+
reason = (
|
| 211 |
+
"Pyarrow only supports a file path as an input and line delimited json"
|
| 212 |
+
"and doesn't support chunksize parameter."
|
| 213 |
+
)
|
| 214 |
+
request.applymarker(pytest.mark.xfail(reason=reason, raises=ValueError))
|
| 215 |
+
|
| 216 |
+
df = DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
|
| 217 |
+
df.to_json(temp_file, lines=True, orient="records")
|
| 218 |
+
with read_json(temp_file, lines=True, chunksize=1, engine=engine) as reader:
|
| 219 |
+
chunked = pd.concat(reader)
|
| 220 |
+
unchunked = read_json(temp_file, lines=True, engine=engine)
|
| 221 |
+
tm.assert_frame_equal(unchunked, chunked)
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
@pytest.mark.parametrize("chunksize", [None, 1])
|
| 225 |
+
def test_readjson_chunks_closes(chunksize, temp_file):
|
| 226 |
+
df = DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
|
| 227 |
+
df.to_json(temp_file, lines=True, orient="records")
|
| 228 |
+
reader = JsonReader(
|
| 229 |
+
temp_file,
|
| 230 |
+
orient=None,
|
| 231 |
+
typ="frame",
|
| 232 |
+
dtype=True,
|
| 233 |
+
convert_axes=True,
|
| 234 |
+
convert_dates=True,
|
| 235 |
+
keep_default_dates=True,
|
| 236 |
+
precise_float=False,
|
| 237 |
+
date_unit=None,
|
| 238 |
+
encoding=None,
|
| 239 |
+
lines=True,
|
| 240 |
+
chunksize=chunksize,
|
| 241 |
+
compression=None,
|
| 242 |
+
nrows=None,
|
| 243 |
+
)
|
| 244 |
+
with reader:
|
| 245 |
+
reader.read()
|
| 246 |
+
assert reader.handles.handle.closed, (
|
| 247 |
+
f"didn't close stream with chunksize = {chunksize}"
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
@pytest.mark.parametrize("chunksize", [0, -1, 2.2, "foo"])
|
| 252 |
+
def test_readjson_invalid_chunksize(lines_json_df, chunksize, engine):
|
| 253 |
+
msg = r"'chunksize' must be an integer >=1"
|
| 254 |
+
|
| 255 |
+
with pytest.raises(ValueError, match=msg):
|
| 256 |
+
with read_json(
|
| 257 |
+
StringIO(lines_json_df), lines=True, chunksize=chunksize, engine=engine
|
| 258 |
+
) as _:
|
| 259 |
+
pass
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
@pytest.mark.parametrize("chunksize", [None, 1, 2])
|
| 263 |
+
def test_readjson_chunks_multiple_empty_lines(chunksize):
|
| 264 |
+
j = """
|
| 265 |
+
|
| 266 |
+
{"A":1,"B":4}
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
{"A":2,"B":5}
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
{"A":3,"B":6}
|
| 279 |
+
"""
|
| 280 |
+
orig = DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
|
| 281 |
+
test = read_json(StringIO(j), lines=True, chunksize=chunksize)
|
| 282 |
+
if chunksize is not None:
|
| 283 |
+
with test:
|
| 284 |
+
test = pd.concat(test)
|
| 285 |
+
tm.assert_frame_equal(orig, test, obj=f"chunksize: {chunksize}")
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
def test_readjson_unicode(request, monkeypatch, engine, temp_file):
|
| 289 |
+
if engine == "pyarrow":
|
| 290 |
+
# GH 48893
|
| 291 |
+
reason = (
|
| 292 |
+
"Pyarrow only supports a file path as an input and line delimited json"
|
| 293 |
+
"and doesn't support chunksize parameter."
|
| 294 |
+
)
|
| 295 |
+
request.applymarker(pytest.mark.xfail(reason=reason, raises=ValueError))
|
| 296 |
+
|
| 297 |
+
monkeypatch.setattr("locale.getpreferredencoding", lambda do_setlocale: "cp949")
|
| 298 |
+
with open(temp_file, "w", encoding="utf-8") as f:
|
| 299 |
+
f.write('{"£©µÀÆÖÞßéöÿ":["АБВГДабвгд가"]}')
|
| 300 |
+
|
| 301 |
+
result = read_json(temp_file, engine=engine)
|
| 302 |
+
expected = DataFrame({"£©µÀÆÖÞßéöÿ": ["АБВГДабвгд가"]})
|
| 303 |
+
tm.assert_frame_equal(result, expected)
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
@pytest.mark.parametrize("nrows", [1, 2])
|
| 307 |
+
def test_readjson_nrows(nrows, engine):
|
| 308 |
+
# GH 33916
|
| 309 |
+
# Test reading line-format JSON to Series with nrows param
|
| 310 |
+
jsonl = """{"a": 1, "b": 2}
|
| 311 |
+
{"a": 3, "b": 4}
|
| 312 |
+
{"a": 5, "b": 6}
|
| 313 |
+
{"a": 7, "b": 8}"""
|
| 314 |
+
result = read_json(StringIO(jsonl), lines=True, nrows=nrows)
|
| 315 |
+
expected = DataFrame({"a": [1, 3, 5, 7], "b": [2, 4, 6, 8]}).iloc[:nrows]
|
| 316 |
+
tm.assert_frame_equal(result, expected)
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
@pytest.mark.parametrize("nrows,chunksize", [(2, 2), (4, 2)])
|
| 320 |
+
def test_readjson_nrows_chunks(request, nrows, chunksize, engine):
|
| 321 |
+
# GH 33916
|
| 322 |
+
# Test reading line-format JSON to Series with nrows and chunksize param
|
| 323 |
+
if engine == "pyarrow":
|
| 324 |
+
# GH 48893
|
| 325 |
+
reason = (
|
| 326 |
+
"Pyarrow only supports a file path as an input and line delimited json"
|
| 327 |
+
"and doesn't support chunksize parameter."
|
| 328 |
+
)
|
| 329 |
+
request.applymarker(pytest.mark.xfail(reason=reason, raises=ValueError))
|
| 330 |
+
|
| 331 |
+
jsonl = """{"a": 1, "b": 2}
|
| 332 |
+
{"a": 3, "b": 4}
|
| 333 |
+
{"a": 5, "b": 6}
|
| 334 |
+
{"a": 7, "b": 8}"""
|
| 335 |
+
|
| 336 |
+
if engine != "pyarrow":
|
| 337 |
+
with read_json(
|
| 338 |
+
StringIO(jsonl), lines=True, nrows=nrows, chunksize=chunksize, engine=engine
|
| 339 |
+
) as reader:
|
| 340 |
+
chunked = pd.concat(reader)
|
| 341 |
+
else:
|
| 342 |
+
with read_json(
|
| 343 |
+
jsonl, lines=True, nrows=nrows, chunksize=chunksize, engine=engine
|
| 344 |
+
) as reader:
|
| 345 |
+
chunked = pd.concat(reader)
|
| 346 |
+
expected = DataFrame({"a": [1, 3, 5, 7], "b": [2, 4, 6, 8]}).iloc[:nrows]
|
| 347 |
+
tm.assert_frame_equal(chunked, expected)
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
def test_readjson_nrows_requires_lines(engine):
|
| 351 |
+
# GH 33916
|
| 352 |
+
# Test ValueError raised if nrows is set without setting lines in read_json
|
| 353 |
+
jsonl = """{"a": 1, "b": 2}
|
| 354 |
+
{"a": 3, "b": 4}
|
| 355 |
+
{"a": 5, "b": 6}
|
| 356 |
+
{"a": 7, "b": 8}"""
|
| 357 |
+
msg = "nrows can only be passed if lines=True"
|
| 358 |
+
with pytest.raises(ValueError, match=msg):
|
| 359 |
+
read_json(jsonl, lines=False, nrows=2, engine=engine)
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
def test_readjson_lines_chunks_fileurl(request, datapath, engine):
|
| 363 |
+
# GH 27135
|
| 364 |
+
# Test reading line-format JSON from file url
|
| 365 |
+
if engine == "pyarrow":
|
| 366 |
+
# GH 48893
|
| 367 |
+
reason = (
|
| 368 |
+
"Pyarrow only supports a file path as an input and line delimited json"
|
| 369 |
+
"and doesn't support chunksize parameter."
|
| 370 |
+
)
|
| 371 |
+
request.applymarker(pytest.mark.xfail(reason=reason, raises=ValueError))
|
| 372 |
+
|
| 373 |
+
df_list_expected = [
|
| 374 |
+
DataFrame([[1, 2]], columns=["a", "b"], index=[0]),
|
| 375 |
+
DataFrame([[3, 4]], columns=["a", "b"], index=[1]),
|
| 376 |
+
DataFrame([[5, 6]], columns=["a", "b"], index=[2]),
|
| 377 |
+
]
|
| 378 |
+
os_path = datapath("io", "json", "data", "line_delimited.json")
|
| 379 |
+
file_url = Path(os_path).as_uri()
|
| 380 |
+
with read_json(file_url, lines=True, chunksize=1, engine=engine) as url_reader:
|
| 381 |
+
for index, chuck in enumerate(url_reader):
|
| 382 |
+
tm.assert_frame_equal(chuck, df_list_expected[index])
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
def test_chunksize_is_incremental():
|
| 386 |
+
# See https://github.com/pandas-dev/pandas/issues/34548
|
| 387 |
+
jsonl = (
|
| 388 |
+
"""{"a": 1, "b": 2}
|
| 389 |
+
{"a": 3, "b": 4}
|
| 390 |
+
{"a": 5, "b": 6}
|
| 391 |
+
{"a": 7, "b": 8}\n"""
|
| 392 |
+
* 1000
|
| 393 |
+
)
|
| 394 |
+
|
| 395 |
+
class MyReader:
|
| 396 |
+
def __init__(self, contents) -> None:
|
| 397 |
+
self.read_count = 0
|
| 398 |
+
self.stringio = StringIO(contents)
|
| 399 |
+
|
| 400 |
+
def read(self, *args):
|
| 401 |
+
self.read_count += 1
|
| 402 |
+
return self.stringio.read(*args)
|
| 403 |
+
|
| 404 |
+
def __iter__(self) -> Iterator:
|
| 405 |
+
self.read_count += 1
|
| 406 |
+
return iter(self.stringio)
|
| 407 |
+
|
| 408 |
+
reader = MyReader(jsonl)
|
| 409 |
+
assert len(list(read_json(reader, lines=True, chunksize=100))) > 1
|
| 410 |
+
assert reader.read_count > 10
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
@pytest.mark.parametrize("orient_", ["split", "index", "table"])
|
| 414 |
+
def test_to_json_append_orient(orient_):
|
| 415 |
+
# GH 35849
|
| 416 |
+
# Test ValueError when orient is not 'records'
|
| 417 |
+
df = DataFrame({"col1": [1, 2], "col2": ["a", "b"]})
|
| 418 |
+
msg = (
|
| 419 |
+
r"mode='a' \(append\) is only supported when "
|
| 420 |
+
"lines is True and orient is 'records'"
|
| 421 |
+
)
|
| 422 |
+
with pytest.raises(ValueError, match=msg):
|
| 423 |
+
df.to_json(mode="a", orient=orient_)
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
def test_to_json_append_lines():
|
| 427 |
+
# GH 35849
|
| 428 |
+
# Test ValueError when lines is not True
|
| 429 |
+
df = DataFrame({"col1": [1, 2], "col2": ["a", "b"]})
|
| 430 |
+
msg = (
|
| 431 |
+
r"mode='a' \(append\) is only supported when "
|
| 432 |
+
"lines is True and orient is 'records'"
|
| 433 |
+
)
|
| 434 |
+
with pytest.raises(ValueError, match=msg):
|
| 435 |
+
df.to_json(mode="a", lines=False, orient="records")
|
| 436 |
+
|
| 437 |
+
|
| 438 |
+
@pytest.mark.parametrize("mode_", ["r", "x"])
|
| 439 |
+
def test_to_json_append_mode(mode_):
|
| 440 |
+
# GH 35849
|
| 441 |
+
# Test ValueError when mode is not supported option
|
| 442 |
+
df = DataFrame({"col1": [1, 2], "col2": ["a", "b"]})
|
| 443 |
+
msg = (
|
| 444 |
+
f"mode={mode_} is not a valid option.Only 'w' and 'a' are currently supported."
|
| 445 |
+
)
|
| 446 |
+
with pytest.raises(ValueError, match=msg):
|
| 447 |
+
df.to_json(mode=mode_, lines=False, orient="records")
|
| 448 |
+
|
| 449 |
+
|
| 450 |
+
def test_to_json_append_output_consistent_columns(temp_file):
|
| 451 |
+
# GH 35849
|
| 452 |
+
# Testing that resulting output reads in as expected.
|
| 453 |
+
# Testing same columns, new rows
|
| 454 |
+
df1 = DataFrame({"col1": [1, 2], "col2": ["a", "b"]})
|
| 455 |
+
df2 = DataFrame({"col1": [3, 4], "col2": ["c", "d"]})
|
| 456 |
+
|
| 457 |
+
expected = DataFrame({"col1": [1, 2, 3, 4], "col2": ["a", "b", "c", "d"]})
|
| 458 |
+
# Save dataframes to the same file
|
| 459 |
+
df1.to_json(temp_file, lines=True, orient="records")
|
| 460 |
+
df2.to_json(temp_file, mode="a", lines=True, orient="records")
|
| 461 |
+
|
| 462 |
+
# Read path file
|
| 463 |
+
result = read_json(temp_file, lines=True)
|
| 464 |
+
tm.assert_frame_equal(result, expected)
|
| 465 |
+
|
| 466 |
+
|
| 467 |
+
def test_to_json_append_output_inconsistent_columns(temp_file):
|
| 468 |
+
# GH 35849
|
| 469 |
+
# Testing that resulting output reads in as expected.
|
| 470 |
+
# Testing one new column, one old column, new rows
|
| 471 |
+
df1 = DataFrame({"col1": [1, 2], "col2": ["a", "b"]})
|
| 472 |
+
df3 = DataFrame({"col2": ["e", "f"], "col3": ["!", "#"]})
|
| 473 |
+
|
| 474 |
+
expected = DataFrame(
|
| 475 |
+
{
|
| 476 |
+
"col1": [1, 2, None, None],
|
| 477 |
+
"col2": ["a", "b", "e", "f"],
|
| 478 |
+
"col3": [np.nan, np.nan, "!", "#"],
|
| 479 |
+
}
|
| 480 |
+
)
|
| 481 |
+
# Save dataframes to the same file
|
| 482 |
+
df1.to_json(temp_file, mode="a", lines=True, orient="records")
|
| 483 |
+
df3.to_json(temp_file, mode="a", lines=True, orient="records")
|
| 484 |
+
|
| 485 |
+
# Read path file
|
| 486 |
+
result = read_json(temp_file, lines=True)
|
| 487 |
+
tm.assert_frame_equal(result, expected)
|
| 488 |
+
|
| 489 |
+
|
| 490 |
+
def test_to_json_append_output_different_columns(temp_file):
|
| 491 |
+
# GH 35849
|
| 492 |
+
# Testing that resulting output reads in as expected.
|
| 493 |
+
# Testing same, differing and new columns
|
| 494 |
+
df1 = DataFrame({"col1": [1, 2], "col2": ["a", "b"]})
|
| 495 |
+
df2 = DataFrame({"col1": [3, 4], "col2": ["c", "d"]})
|
| 496 |
+
df3 = DataFrame({"col2": ["e", "f"], "col3": ["!", "#"]})
|
| 497 |
+
df4 = DataFrame({"col4": [True, False]})
|
| 498 |
+
|
| 499 |
+
expected = DataFrame(
|
| 500 |
+
{
|
| 501 |
+
"col1": [1, 2, 3, 4, None, None, None, None],
|
| 502 |
+
"col2": ["a", "b", "c", "d", "e", "f", np.nan, np.nan],
|
| 503 |
+
"col3": [np.nan, np.nan, np.nan, np.nan, "!", "#", np.nan, np.nan],
|
| 504 |
+
"col4": [None, None, None, None, None, None, True, False],
|
| 505 |
+
}
|
| 506 |
+
).astype({"col4": "float"})
|
| 507 |
+
# Save dataframes to the same file
|
| 508 |
+
df1.to_json(temp_file, mode="a", lines=True, orient="records")
|
| 509 |
+
df2.to_json(temp_file, mode="a", lines=True, orient="records")
|
| 510 |
+
df3.to_json(temp_file, mode="a", lines=True, orient="records")
|
| 511 |
+
df4.to_json(temp_file, mode="a", lines=True, orient="records")
|
| 512 |
+
|
| 513 |
+
# Read path file
|
| 514 |
+
result = read_json(temp_file, lines=True)
|
| 515 |
+
tm.assert_frame_equal(result, expected)
|
| 516 |
+
|
| 517 |
+
|
| 518 |
+
def test_to_json_append_output_different_columns_reordered(temp_file):
|
| 519 |
+
# GH 35849
|
| 520 |
+
# Testing that resulting output reads in as expected.
|
| 521 |
+
# Testing specific result column order.
|
| 522 |
+
df1 = DataFrame({"col1": [1, 2], "col2": ["a", "b"]})
|
| 523 |
+
df2 = DataFrame({"col1": [3, 4], "col2": ["c", "d"]})
|
| 524 |
+
df3 = DataFrame({"col2": ["e", "f"], "col3": ["!", "#"]})
|
| 525 |
+
df4 = DataFrame({"col4": [True, False]})
|
| 526 |
+
|
| 527 |
+
# df4, df3, df2, df1 (in that order)
|
| 528 |
+
expected = DataFrame(
|
| 529 |
+
{
|
| 530 |
+
"col4": [True, False, None, None, None, None, None, None],
|
| 531 |
+
"col2": [np.nan, np.nan, "e", "f", "c", "d", "a", "b"],
|
| 532 |
+
"col3": [np.nan, np.nan, "!", "#", np.nan, np.nan, np.nan, np.nan],
|
| 533 |
+
"col1": [None, None, None, None, 3, 4, 1, 2],
|
| 534 |
+
}
|
| 535 |
+
).astype({"col4": "float"})
|
| 536 |
+
# Save dataframes to the same file
|
| 537 |
+
df4.to_json(temp_file, mode="a", lines=True, orient="records")
|
| 538 |
+
df3.to_json(temp_file, mode="a", lines=True, orient="records")
|
| 539 |
+
df2.to_json(temp_file, mode="a", lines=True, orient="records")
|
| 540 |
+
df1.to_json(temp_file, mode="a", lines=True, orient="records")
|
| 541 |
+
|
| 542 |
+
# Read path file
|
| 543 |
+
result = read_json(temp_file, lines=True)
|
| 544 |
+
tm.assert_frame_equal(result, expected)
|
.venv/lib/python3.13/site-packages/pandas/tests/io/json/test_ujson.py
ADDED
|
@@ -0,0 +1,1045 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import calendar
|
| 2 |
+
import datetime
|
| 3 |
+
import decimal
|
| 4 |
+
import json
|
| 5 |
+
import locale
|
| 6 |
+
import math
|
| 7 |
+
import re
|
| 8 |
+
import time
|
| 9 |
+
|
| 10 |
+
import dateutil
|
| 11 |
+
import numpy as np
|
| 12 |
+
import pytest
|
| 13 |
+
|
| 14 |
+
import pandas._libs.json as ujson
|
| 15 |
+
from pandas.compat import IS64
|
| 16 |
+
|
| 17 |
+
from pandas import (
|
| 18 |
+
DataFrame,
|
| 19 |
+
DatetimeIndex,
|
| 20 |
+
Index,
|
| 21 |
+
NaT,
|
| 22 |
+
PeriodIndex,
|
| 23 |
+
Series,
|
| 24 |
+
Timedelta,
|
| 25 |
+
Timestamp,
|
| 26 |
+
date_range,
|
| 27 |
+
)
|
| 28 |
+
import pandas._testing as tm
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _clean_dict(d):
|
| 32 |
+
"""
|
| 33 |
+
Sanitize dictionary for JSON by converting all keys to strings.
|
| 34 |
+
|
| 35 |
+
Parameters
|
| 36 |
+
----------
|
| 37 |
+
d : dict
|
| 38 |
+
The dictionary to convert.
|
| 39 |
+
|
| 40 |
+
Returns
|
| 41 |
+
-------
|
| 42 |
+
cleaned_dict : dict
|
| 43 |
+
"""
|
| 44 |
+
return {str(k): v for k, v in d.items()}
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@pytest.fixture(
|
| 48 |
+
params=[None, "split", "records", "values", "index"] # Column indexed by default.
|
| 49 |
+
)
|
| 50 |
+
def orient(request):
|
| 51 |
+
return request.param
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class TestUltraJSONTests:
|
| 55 |
+
@pytest.mark.skipif(not IS64, reason="not compliant on 32-bit, xref #15865")
|
| 56 |
+
@pytest.mark.parametrize(
|
| 57 |
+
"value, double_precision",
|
| 58 |
+
[
|
| 59 |
+
("1337.1337", 15),
|
| 60 |
+
("0.95", 1),
|
| 61 |
+
("0.94", 1),
|
| 62 |
+
("1.95", 1),
|
| 63 |
+
("-1.95", 1),
|
| 64 |
+
("0.995", 2),
|
| 65 |
+
("0.9995", 3),
|
| 66 |
+
("0.99999999999999944", 15),
|
| 67 |
+
],
|
| 68 |
+
)
|
| 69 |
+
def test_encode_decimal(self, value, double_precision):
|
| 70 |
+
sut = decimal.Decimal(value)
|
| 71 |
+
encoded = ujson.ujson_dumps(sut, double_precision=double_precision)
|
| 72 |
+
decoded = ujson.ujson_loads(encoded)
|
| 73 |
+
assert decoded == value
|
| 74 |
+
|
| 75 |
+
@pytest.mark.parametrize("ensure_ascii", [True, False])
|
| 76 |
+
def test_encode_string_conversion(self, ensure_ascii):
|
| 77 |
+
string_input = "A string \\ / \b \f \n \r \t </script> &"
|
| 78 |
+
not_html_encoded = '"A string \\\\ \\/ \\b \\f \\n \\r \\t <\\/script> &"'
|
| 79 |
+
html_encoded = (
|
| 80 |
+
'"A string \\\\ \\/ \\b \\f \\n \\r \\t \\u003c\\/script\\u003e \\u0026"'
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
def helper(expected_output, **encode_kwargs):
|
| 84 |
+
output = ujson.ujson_dumps(
|
| 85 |
+
string_input, ensure_ascii=ensure_ascii, **encode_kwargs
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
assert output == expected_output
|
| 89 |
+
assert string_input == json.loads(output)
|
| 90 |
+
assert string_input == ujson.ujson_loads(output)
|
| 91 |
+
|
| 92 |
+
# Default behavior assumes encode_html_chars=False.
|
| 93 |
+
helper(not_html_encoded)
|
| 94 |
+
|
| 95 |
+
# Make sure explicit encode_html_chars=False works.
|
| 96 |
+
helper(not_html_encoded, encode_html_chars=False)
|
| 97 |
+
|
| 98 |
+
# Make sure explicit encode_html_chars=True does the encoding.
|
| 99 |
+
helper(html_encoded, encode_html_chars=True)
|
| 100 |
+
|
| 101 |
+
@pytest.mark.parametrize(
|
| 102 |
+
"long_number", [-4342969734183514, -12345678901234.56789012, -528656961.4399388]
|
| 103 |
+
)
|
| 104 |
+
def test_double_long_numbers(self, long_number):
|
| 105 |
+
sut = {"a": long_number}
|
| 106 |
+
encoded = ujson.ujson_dumps(sut, double_precision=15)
|
| 107 |
+
|
| 108 |
+
decoded = ujson.ujson_loads(encoded)
|
| 109 |
+
assert sut == decoded
|
| 110 |
+
|
| 111 |
+
def test_encode_non_c_locale(self):
|
| 112 |
+
lc_category = locale.LC_NUMERIC
|
| 113 |
+
|
| 114 |
+
# We just need one of these locales to work.
|
| 115 |
+
for new_locale in ("it_IT.UTF-8", "Italian_Italy"):
|
| 116 |
+
if tm.can_set_locale(new_locale, lc_category):
|
| 117 |
+
with tm.set_locale(new_locale, lc_category):
|
| 118 |
+
assert ujson.ujson_loads(ujson.ujson_dumps(4.78e60)) == 4.78e60
|
| 119 |
+
assert ujson.ujson_loads("4.78", precise_float=True) == 4.78
|
| 120 |
+
break
|
| 121 |
+
|
| 122 |
+
def test_decimal_decode_test_precise(self):
|
| 123 |
+
sut = {"a": 4.56}
|
| 124 |
+
encoded = ujson.ujson_dumps(sut)
|
| 125 |
+
decoded = ujson.ujson_loads(encoded, precise_float=True)
|
| 126 |
+
assert sut == decoded
|
| 127 |
+
|
| 128 |
+
def test_encode_double_tiny_exponential(self):
|
| 129 |
+
num = 1e-40
|
| 130 |
+
assert num == ujson.ujson_loads(ujson.ujson_dumps(num))
|
| 131 |
+
num = 1e-100
|
| 132 |
+
assert num == ujson.ujson_loads(ujson.ujson_dumps(num))
|
| 133 |
+
num = -1e-45
|
| 134 |
+
assert num == ujson.ujson_loads(ujson.ujson_dumps(num))
|
| 135 |
+
num = -1e-145
|
| 136 |
+
assert np.allclose(num, ujson.ujson_loads(ujson.ujson_dumps(num)))
|
| 137 |
+
|
| 138 |
+
@pytest.mark.parametrize("unicode_key", ["key1", "بن"])
|
| 139 |
+
def test_encode_dict_with_unicode_keys(self, unicode_key):
|
| 140 |
+
unicode_dict = {unicode_key: "value1"}
|
| 141 |
+
assert unicode_dict == ujson.ujson_loads(ujson.ujson_dumps(unicode_dict))
|
| 142 |
+
|
| 143 |
+
@pytest.mark.parametrize("double_input", [math.pi, -math.pi])
|
| 144 |
+
def test_encode_double_conversion(self, double_input):
|
| 145 |
+
output = ujson.ujson_dumps(double_input)
|
| 146 |
+
assert round(double_input, 5) == round(json.loads(output), 5)
|
| 147 |
+
assert round(double_input, 5) == round(ujson.ujson_loads(output), 5)
|
| 148 |
+
|
| 149 |
+
def test_encode_with_decimal(self):
|
| 150 |
+
decimal_input = 1.0
|
| 151 |
+
output = ujson.ujson_dumps(decimal_input)
|
| 152 |
+
|
| 153 |
+
assert output == "1.0"
|
| 154 |
+
|
| 155 |
+
def test_encode_array_of_nested_arrays(self):
|
| 156 |
+
nested_input = [[[[]]]] * 20
|
| 157 |
+
output = ujson.ujson_dumps(nested_input)
|
| 158 |
+
|
| 159 |
+
assert nested_input == json.loads(output)
|
| 160 |
+
assert nested_input == ujson.ujson_loads(output)
|
| 161 |
+
|
| 162 |
+
def test_encode_array_of_doubles(self):
|
| 163 |
+
doubles_input = [31337.31337, 31337.31337, 31337.31337, 31337.31337] * 10
|
| 164 |
+
output = ujson.ujson_dumps(doubles_input)
|
| 165 |
+
|
| 166 |
+
assert doubles_input == json.loads(output)
|
| 167 |
+
assert doubles_input == ujson.ujson_loads(output)
|
| 168 |
+
|
| 169 |
+
def test_double_precision(self):
|
| 170 |
+
double_input = 30.012345678901234
|
| 171 |
+
output = ujson.ujson_dumps(double_input, double_precision=15)
|
| 172 |
+
|
| 173 |
+
assert double_input == json.loads(output)
|
| 174 |
+
assert double_input == ujson.ujson_loads(output)
|
| 175 |
+
|
| 176 |
+
for double_precision in (3, 9):
|
| 177 |
+
output = ujson.ujson_dumps(double_input, double_precision=double_precision)
|
| 178 |
+
rounded_input = round(double_input, double_precision)
|
| 179 |
+
|
| 180 |
+
assert rounded_input == json.loads(output)
|
| 181 |
+
assert rounded_input == ujson.ujson_loads(output)
|
| 182 |
+
|
| 183 |
+
@pytest.mark.parametrize(
|
| 184 |
+
"invalid_val",
|
| 185 |
+
[
|
| 186 |
+
20,
|
| 187 |
+
-1,
|
| 188 |
+
"9",
|
| 189 |
+
None,
|
| 190 |
+
],
|
| 191 |
+
)
|
| 192 |
+
def test_invalid_double_precision(self, invalid_val):
|
| 193 |
+
double_input = 30.12345678901234567890
|
| 194 |
+
expected_exception = ValueError if isinstance(invalid_val, int) else TypeError
|
| 195 |
+
msg = (
|
| 196 |
+
r"Invalid value '.*' for option 'double_precision', max is '15'|"
|
| 197 |
+
r"an integer is required \(got type |"
|
| 198 |
+
r"object cannot be interpreted as an integer"
|
| 199 |
+
)
|
| 200 |
+
with pytest.raises(expected_exception, match=msg):
|
| 201 |
+
ujson.ujson_dumps(double_input, double_precision=invalid_val)
|
| 202 |
+
|
| 203 |
+
def test_encode_string_conversion2(self):
|
| 204 |
+
string_input = "A string \\ / \b \f \n \r \t"
|
| 205 |
+
output = ujson.ujson_dumps(string_input)
|
| 206 |
+
|
| 207 |
+
assert string_input == json.loads(output)
|
| 208 |
+
assert string_input == ujson.ujson_loads(output)
|
| 209 |
+
assert output == '"A string \\\\ \\/ \\b \\f \\n \\r \\t"'
|
| 210 |
+
|
| 211 |
+
@pytest.mark.parametrize(
|
| 212 |
+
"unicode_input",
|
| 213 |
+
["Räksmörgås اسامة بن محمد بن عوض بن لادن", "\xe6\x97\xa5\xd1\x88"],
|
| 214 |
+
)
|
| 215 |
+
def test_encode_unicode_conversion(self, unicode_input):
|
| 216 |
+
enc = ujson.ujson_dumps(unicode_input)
|
| 217 |
+
dec = ujson.ujson_loads(enc)
|
| 218 |
+
|
| 219 |
+
assert enc == json.dumps(unicode_input)
|
| 220 |
+
assert dec == json.loads(enc)
|
| 221 |
+
|
| 222 |
+
def test_encode_control_escaping(self):
|
| 223 |
+
escaped_input = "\x19"
|
| 224 |
+
enc = ujson.ujson_dumps(escaped_input)
|
| 225 |
+
dec = ujson.ujson_loads(enc)
|
| 226 |
+
|
| 227 |
+
assert escaped_input == dec
|
| 228 |
+
assert enc == json.dumps(escaped_input)
|
| 229 |
+
|
| 230 |
+
def test_encode_unicode_surrogate_pair(self):
|
| 231 |
+
surrogate_input = "\xf0\x90\x8d\x86"
|
| 232 |
+
enc = ujson.ujson_dumps(surrogate_input)
|
| 233 |
+
dec = ujson.ujson_loads(enc)
|
| 234 |
+
|
| 235 |
+
assert enc == json.dumps(surrogate_input)
|
| 236 |
+
assert dec == json.loads(enc)
|
| 237 |
+
|
| 238 |
+
def test_encode_unicode_4bytes_utf8(self):
|
| 239 |
+
four_bytes_input = "\xf0\x91\x80\xb0TRAILINGNORMAL"
|
| 240 |
+
enc = ujson.ujson_dumps(four_bytes_input)
|
| 241 |
+
dec = ujson.ujson_loads(enc)
|
| 242 |
+
|
| 243 |
+
assert enc == json.dumps(four_bytes_input)
|
| 244 |
+
assert dec == json.loads(enc)
|
| 245 |
+
|
| 246 |
+
def test_encode_unicode_4bytes_utf8highest(self):
|
| 247 |
+
four_bytes_input = "\xf3\xbf\xbf\xbfTRAILINGNORMAL"
|
| 248 |
+
enc = ujson.ujson_dumps(four_bytes_input)
|
| 249 |
+
|
| 250 |
+
dec = ujson.ujson_loads(enc)
|
| 251 |
+
|
| 252 |
+
assert enc == json.dumps(four_bytes_input)
|
| 253 |
+
assert dec == json.loads(enc)
|
| 254 |
+
|
| 255 |
+
def test_encode_unicode_error(self):
|
| 256 |
+
string = "'\udac0'"
|
| 257 |
+
msg = (
|
| 258 |
+
r"'utf-8' codec can't encode character '\\udac0' "
|
| 259 |
+
r"in position 1: surrogates not allowed"
|
| 260 |
+
)
|
| 261 |
+
with pytest.raises(UnicodeEncodeError, match=msg):
|
| 262 |
+
ujson.ujson_dumps([string])
|
| 263 |
+
|
| 264 |
+
def test_encode_array_in_array(self):
|
| 265 |
+
arr_in_arr_input = [[[[]]]]
|
| 266 |
+
output = ujson.ujson_dumps(arr_in_arr_input)
|
| 267 |
+
|
| 268 |
+
assert arr_in_arr_input == json.loads(output)
|
| 269 |
+
assert output == json.dumps(arr_in_arr_input)
|
| 270 |
+
assert arr_in_arr_input == ujson.ujson_loads(output)
|
| 271 |
+
|
| 272 |
+
@pytest.mark.parametrize(
|
| 273 |
+
"num_input",
|
| 274 |
+
[
|
| 275 |
+
31337,
|
| 276 |
+
-31337, # Negative number.
|
| 277 |
+
-9223372036854775808, # Large negative number.
|
| 278 |
+
],
|
| 279 |
+
)
|
| 280 |
+
def test_encode_num_conversion(self, num_input):
|
| 281 |
+
output = ujson.ujson_dumps(num_input)
|
| 282 |
+
assert num_input == json.loads(output)
|
| 283 |
+
assert output == json.dumps(num_input)
|
| 284 |
+
assert num_input == ujson.ujson_loads(output)
|
| 285 |
+
|
| 286 |
+
def test_encode_list_conversion(self):
|
| 287 |
+
list_input = [1, 2, 3, 4]
|
| 288 |
+
output = ujson.ujson_dumps(list_input)
|
| 289 |
+
|
| 290 |
+
assert list_input == json.loads(output)
|
| 291 |
+
assert list_input == ujson.ujson_loads(output)
|
| 292 |
+
|
| 293 |
+
def test_encode_dict_conversion(self):
|
| 294 |
+
dict_input = {"k1": 1, "k2": 2, "k3": 3, "k4": 4}
|
| 295 |
+
output = ujson.ujson_dumps(dict_input)
|
| 296 |
+
|
| 297 |
+
assert dict_input == json.loads(output)
|
| 298 |
+
assert dict_input == ujson.ujson_loads(output)
|
| 299 |
+
|
| 300 |
+
@pytest.mark.parametrize("builtin_value", [None, True, False])
|
| 301 |
+
def test_encode_builtin_values_conversion(self, builtin_value):
|
| 302 |
+
output = ujson.ujson_dumps(builtin_value)
|
| 303 |
+
assert builtin_value == json.loads(output)
|
| 304 |
+
assert output == json.dumps(builtin_value)
|
| 305 |
+
assert builtin_value == ujson.ujson_loads(output)
|
| 306 |
+
|
| 307 |
+
def test_encode_datetime_conversion(self):
|
| 308 |
+
datetime_input = datetime.datetime.fromtimestamp(time.time())
|
| 309 |
+
output = ujson.ujson_dumps(datetime_input, date_unit="s")
|
| 310 |
+
expected = calendar.timegm(datetime_input.utctimetuple())
|
| 311 |
+
|
| 312 |
+
assert int(expected) == json.loads(output)
|
| 313 |
+
assert int(expected) == ujson.ujson_loads(output)
|
| 314 |
+
|
| 315 |
+
def test_encode_date_conversion(self):
|
| 316 |
+
date_input = datetime.date.fromtimestamp(time.time())
|
| 317 |
+
output = ujson.ujson_dumps(date_input, date_unit="s")
|
| 318 |
+
|
| 319 |
+
tup = (date_input.year, date_input.month, date_input.day, 0, 0, 0)
|
| 320 |
+
expected = calendar.timegm(tup)
|
| 321 |
+
|
| 322 |
+
assert int(expected) == json.loads(output)
|
| 323 |
+
assert int(expected) == ujson.ujson_loads(output)
|
| 324 |
+
|
| 325 |
+
@pytest.mark.parametrize(
|
| 326 |
+
"test",
|
| 327 |
+
[datetime.time(), datetime.time(1, 2, 3), datetime.time(10, 12, 15, 343243)],
|
| 328 |
+
)
|
| 329 |
+
def test_encode_time_conversion_basic(self, test):
|
| 330 |
+
output = ujson.ujson_dumps(test)
|
| 331 |
+
expected = f'"{test.isoformat()}"'
|
| 332 |
+
assert expected == output
|
| 333 |
+
|
| 334 |
+
def test_encode_time_conversion_pytz(self):
|
| 335 |
+
# see gh-11473: to_json segfaults with timezone-aware datetimes
|
| 336 |
+
pytz = pytest.importorskip("pytz")
|
| 337 |
+
test = datetime.time(10, 12, 15, 343243, pytz.utc)
|
| 338 |
+
output = ujson.ujson_dumps(test)
|
| 339 |
+
expected = f'"{test.isoformat()}"'
|
| 340 |
+
assert expected == output
|
| 341 |
+
|
| 342 |
+
def test_encode_time_conversion_dateutil(self):
|
| 343 |
+
# see gh-11473: to_json segfaults with timezone-aware datetimes
|
| 344 |
+
test = datetime.time(10, 12, 15, 343243, dateutil.tz.tzutc())
|
| 345 |
+
output = ujson.ujson_dumps(test)
|
| 346 |
+
expected = f'"{test.isoformat()}"'
|
| 347 |
+
assert expected == output
|
| 348 |
+
|
| 349 |
+
@pytest.mark.parametrize(
|
| 350 |
+
"decoded_input", [NaT, np.datetime64("NaT", "ns"), np.nan, np.inf, -np.inf]
|
| 351 |
+
)
|
| 352 |
+
def test_encode_as_null(self, decoded_input):
|
| 353 |
+
assert ujson.ujson_dumps(decoded_input) == "null", "Expected null"
|
| 354 |
+
|
| 355 |
+
def test_datetime_units(self):
|
| 356 |
+
val = datetime.datetime(2013, 8, 17, 21, 17, 12, 215504)
|
| 357 |
+
stamp = Timestamp(val).as_unit("ns")
|
| 358 |
+
|
| 359 |
+
roundtrip = ujson.ujson_loads(ujson.ujson_dumps(val, date_unit="s"))
|
| 360 |
+
assert roundtrip == stamp._value // 10**9
|
| 361 |
+
|
| 362 |
+
roundtrip = ujson.ujson_loads(ujson.ujson_dumps(val, date_unit="ms"))
|
| 363 |
+
assert roundtrip == stamp._value // 10**6
|
| 364 |
+
|
| 365 |
+
roundtrip = ujson.ujson_loads(ujson.ujson_dumps(val, date_unit="us"))
|
| 366 |
+
assert roundtrip == stamp._value // 10**3
|
| 367 |
+
|
| 368 |
+
roundtrip = ujson.ujson_loads(ujson.ujson_dumps(val, date_unit="ns"))
|
| 369 |
+
assert roundtrip == stamp._value
|
| 370 |
+
|
| 371 |
+
msg = "Invalid value 'foo' for option 'date_unit'"
|
| 372 |
+
with pytest.raises(ValueError, match=msg):
|
| 373 |
+
ujson.ujson_dumps(val, date_unit="foo")
|
| 374 |
+
|
| 375 |
+
def test_encode_to_utf8(self):
|
| 376 |
+
unencoded = "\xe6\x97\xa5\xd1\x88"
|
| 377 |
+
|
| 378 |
+
enc = ujson.ujson_dumps(unencoded, ensure_ascii=False)
|
| 379 |
+
dec = ujson.ujson_loads(enc)
|
| 380 |
+
|
| 381 |
+
assert enc == json.dumps(unencoded, ensure_ascii=False)
|
| 382 |
+
assert dec == json.loads(enc)
|
| 383 |
+
|
| 384 |
+
def test_decode_from_unicode(self):
|
| 385 |
+
unicode_input = '{"obj": 31337}'
|
| 386 |
+
|
| 387 |
+
dec1 = ujson.ujson_loads(unicode_input)
|
| 388 |
+
dec2 = ujson.ujson_loads(str(unicode_input))
|
| 389 |
+
|
| 390 |
+
assert dec1 == dec2
|
| 391 |
+
|
| 392 |
+
def test_encode_recursion_max(self):
|
| 393 |
+
# 8 is the max recursion depth
|
| 394 |
+
|
| 395 |
+
class O2:
|
| 396 |
+
member = 0
|
| 397 |
+
|
| 398 |
+
class O1:
|
| 399 |
+
member = 0
|
| 400 |
+
|
| 401 |
+
decoded_input = O1()
|
| 402 |
+
decoded_input.member = O2()
|
| 403 |
+
decoded_input.member.member = decoded_input
|
| 404 |
+
|
| 405 |
+
with pytest.raises(OverflowError, match="Maximum recursion level reached"):
|
| 406 |
+
ujson.ujson_dumps(decoded_input)
|
| 407 |
+
|
| 408 |
+
def test_decode_jibberish(self):
|
| 409 |
+
jibberish = "fdsa sda v9sa fdsa"
|
| 410 |
+
msg = "Unexpected character found when decoding 'false'"
|
| 411 |
+
with pytest.raises(ValueError, match=msg):
|
| 412 |
+
ujson.ujson_loads(jibberish)
|
| 413 |
+
|
| 414 |
+
@pytest.mark.parametrize(
|
| 415 |
+
"broken_json",
|
| 416 |
+
[
|
| 417 |
+
"[", # Broken array start.
|
| 418 |
+
"{", # Broken object start.
|
| 419 |
+
"]", # Broken array end.
|
| 420 |
+
"}", # Broken object end.
|
| 421 |
+
],
|
| 422 |
+
)
|
| 423 |
+
def test_decode_broken_json(self, broken_json):
|
| 424 |
+
msg = "Expected object or value"
|
| 425 |
+
with pytest.raises(ValueError, match=msg):
|
| 426 |
+
ujson.ujson_loads(broken_json)
|
| 427 |
+
|
| 428 |
+
@pytest.mark.parametrize("too_big_char", ["[", "{"])
|
| 429 |
+
def test_decode_depth_too_big(self, too_big_char):
|
| 430 |
+
with pytest.raises(ValueError, match="Reached object decoding depth limit"):
|
| 431 |
+
ujson.ujson_loads(too_big_char * (1024 * 1024))
|
| 432 |
+
|
| 433 |
+
@pytest.mark.parametrize(
|
| 434 |
+
"bad_string",
|
| 435 |
+
[
|
| 436 |
+
'"TESTING', # Unterminated.
|
| 437 |
+
'"TESTING\\"', # Unterminated escape.
|
| 438 |
+
"tru", # Broken True.
|
| 439 |
+
"fa", # Broken False.
|
| 440 |
+
"n", # Broken None.
|
| 441 |
+
],
|
| 442 |
+
)
|
| 443 |
+
def test_decode_bad_string(self, bad_string):
|
| 444 |
+
msg = (
|
| 445 |
+
"Unexpected character found when decoding|"
|
| 446 |
+
"Unmatched ''\"' when when decoding 'string'"
|
| 447 |
+
)
|
| 448 |
+
with pytest.raises(ValueError, match=msg):
|
| 449 |
+
ujson.ujson_loads(bad_string)
|
| 450 |
+
|
| 451 |
+
@pytest.mark.parametrize(
|
| 452 |
+
"broken_json, err_msg",
|
| 453 |
+
[
|
| 454 |
+
(
|
| 455 |
+
'{{1337:""}}',
|
| 456 |
+
"Key name of object must be 'string' when decoding 'object'",
|
| 457 |
+
),
|
| 458 |
+
('{{"key":"}', "Unmatched ''\"' when when decoding 'string'"),
|
| 459 |
+
("[[[true", "Unexpected character found when decoding array value (2)"),
|
| 460 |
+
],
|
| 461 |
+
)
|
| 462 |
+
def test_decode_broken_json_leak(self, broken_json, err_msg):
|
| 463 |
+
for _ in range(1000):
|
| 464 |
+
with pytest.raises(ValueError, match=re.escape(err_msg)):
|
| 465 |
+
ujson.ujson_loads(broken_json)
|
| 466 |
+
|
| 467 |
+
@pytest.mark.parametrize(
|
| 468 |
+
"invalid_dict",
|
| 469 |
+
[
|
| 470 |
+
"{{{{31337}}}}", # No key.
|
| 471 |
+
'{{{{"key":}}}}', # No value.
|
| 472 |
+
'{{{{"key"}}}}', # No colon or value.
|
| 473 |
+
],
|
| 474 |
+
)
|
| 475 |
+
def test_decode_invalid_dict(self, invalid_dict):
|
| 476 |
+
msg = (
|
| 477 |
+
"Key name of object must be 'string' when decoding 'object'|"
|
| 478 |
+
"No ':' found when decoding object value|"
|
| 479 |
+
"Expected object or value"
|
| 480 |
+
)
|
| 481 |
+
with pytest.raises(ValueError, match=msg):
|
| 482 |
+
ujson.ujson_loads(invalid_dict)
|
| 483 |
+
|
| 484 |
+
@pytest.mark.parametrize("numeric_int_as_str", ["31337", "-31337"])
|
| 485 |
+
def test_decode_numeric_int(self, numeric_int_as_str):
|
| 486 |
+
assert int(numeric_int_as_str) == ujson.ujson_loads(numeric_int_as_str)
|
| 487 |
+
|
| 488 |
+
def test_encode_null_character(self):
|
| 489 |
+
wrapped_input = "31337 \x00 1337"
|
| 490 |
+
output = ujson.ujson_dumps(wrapped_input)
|
| 491 |
+
|
| 492 |
+
assert wrapped_input == json.loads(output)
|
| 493 |
+
assert output == json.dumps(wrapped_input)
|
| 494 |
+
assert wrapped_input == ujson.ujson_loads(output)
|
| 495 |
+
|
| 496 |
+
alone_input = "\x00"
|
| 497 |
+
output = ujson.ujson_dumps(alone_input)
|
| 498 |
+
|
| 499 |
+
assert alone_input == json.loads(output)
|
| 500 |
+
assert output == json.dumps(alone_input)
|
| 501 |
+
assert alone_input == ujson.ujson_loads(output)
|
| 502 |
+
assert '" \\u0000\\r\\n "' == ujson.ujson_dumps(" \u0000\r\n ")
|
| 503 |
+
|
| 504 |
+
def test_decode_null_character(self):
|
| 505 |
+
wrapped_input = '"31337 \\u0000 31337"'
|
| 506 |
+
assert ujson.ujson_loads(wrapped_input) == json.loads(wrapped_input)
|
| 507 |
+
|
| 508 |
+
def test_encode_list_long_conversion(self):
|
| 509 |
+
long_input = [
|
| 510 |
+
9223372036854775807,
|
| 511 |
+
9223372036854775807,
|
| 512 |
+
9223372036854775807,
|
| 513 |
+
9223372036854775807,
|
| 514 |
+
9223372036854775807,
|
| 515 |
+
9223372036854775807,
|
| 516 |
+
]
|
| 517 |
+
output = ujson.ujson_dumps(long_input)
|
| 518 |
+
|
| 519 |
+
assert long_input == json.loads(output)
|
| 520 |
+
assert long_input == ujson.ujson_loads(output)
|
| 521 |
+
|
| 522 |
+
@pytest.mark.parametrize("long_input", [9223372036854775807, 18446744073709551615])
|
| 523 |
+
def test_encode_long_conversion(self, long_input):
|
| 524 |
+
output = ujson.ujson_dumps(long_input)
|
| 525 |
+
|
| 526 |
+
assert long_input == json.loads(output)
|
| 527 |
+
assert output == json.dumps(long_input)
|
| 528 |
+
assert long_input == ujson.ujson_loads(output)
|
| 529 |
+
|
| 530 |
+
@pytest.mark.parametrize("bigNum", [2**64, -(2**63) - 1])
|
| 531 |
+
def test_dumps_ints_larger_than_maxsize(self, bigNum):
|
| 532 |
+
encoding = ujson.ujson_dumps(bigNum)
|
| 533 |
+
assert str(bigNum) == encoding
|
| 534 |
+
|
| 535 |
+
with pytest.raises(
|
| 536 |
+
ValueError,
|
| 537 |
+
match="Value is too big|Value is too small",
|
| 538 |
+
):
|
| 539 |
+
assert ujson.ujson_loads(encoding) == bigNum
|
| 540 |
+
|
| 541 |
+
@pytest.mark.parametrize(
|
| 542 |
+
"int_exp", ["1337E40", "1.337E40", "1337E+9", "1.337e+40", "1.337E-4"]
|
| 543 |
+
)
|
| 544 |
+
def test_decode_numeric_int_exp(self, int_exp):
|
| 545 |
+
assert ujson.ujson_loads(int_exp) == json.loads(int_exp)
|
| 546 |
+
|
| 547 |
+
def test_loads_non_str_bytes_raises(self):
|
| 548 |
+
msg = "a bytes-like object is required, not 'NoneType'"
|
| 549 |
+
with pytest.raises(TypeError, match=msg):
|
| 550 |
+
ujson.ujson_loads(None)
|
| 551 |
+
|
| 552 |
+
@pytest.mark.parametrize("val", [3590016419, 2**31, 2**32, (2**32) - 1])
|
| 553 |
+
def test_decode_number_with_32bit_sign_bit(self, val):
|
| 554 |
+
# Test that numbers that fit within 32 bits but would have the
|
| 555 |
+
# sign bit set (2**31 <= x < 2**32) are decoded properly.
|
| 556 |
+
doc = f'{{"id": {val}}}'
|
| 557 |
+
assert ujson.ujson_loads(doc)["id"] == val
|
| 558 |
+
|
| 559 |
+
def test_encode_big_escape(self):
|
| 560 |
+
# Make sure no Exception is raised.
|
| 561 |
+
for _ in range(10):
|
| 562 |
+
base = "\u00e5".encode()
|
| 563 |
+
escape_input = base * 1024 * 1024 * 2
|
| 564 |
+
ujson.ujson_dumps(escape_input)
|
| 565 |
+
|
| 566 |
+
def test_decode_big_escape(self):
|
| 567 |
+
# Make sure no Exception is raised.
|
| 568 |
+
for _ in range(10):
|
| 569 |
+
base = "\u00e5".encode()
|
| 570 |
+
quote = b'"'
|
| 571 |
+
|
| 572 |
+
escape_input = quote + (base * 1024 * 1024 * 2) + quote
|
| 573 |
+
ujson.ujson_loads(escape_input)
|
| 574 |
+
|
| 575 |
+
def test_to_dict(self):
|
| 576 |
+
d = {"key": 31337}
|
| 577 |
+
|
| 578 |
+
class DictTest:
|
| 579 |
+
def toDict(self):
|
| 580 |
+
return d
|
| 581 |
+
|
| 582 |
+
o = DictTest()
|
| 583 |
+
output = ujson.ujson_dumps(o)
|
| 584 |
+
|
| 585 |
+
dec = ujson.ujson_loads(output)
|
| 586 |
+
assert dec == d
|
| 587 |
+
|
| 588 |
+
def test_default_handler(self):
|
| 589 |
+
class _TestObject:
|
| 590 |
+
def __init__(self, val) -> None:
|
| 591 |
+
self.val = val
|
| 592 |
+
|
| 593 |
+
@property
|
| 594 |
+
def recursive_attr(self):
|
| 595 |
+
return _TestObject("recursive_attr")
|
| 596 |
+
|
| 597 |
+
def __str__(self) -> str:
|
| 598 |
+
return str(self.val)
|
| 599 |
+
|
| 600 |
+
msg = "Maximum recursion level reached"
|
| 601 |
+
with pytest.raises(OverflowError, match=msg):
|
| 602 |
+
ujson.ujson_dumps(_TestObject("foo"))
|
| 603 |
+
assert '"foo"' == ujson.ujson_dumps(_TestObject("foo"), default_handler=str)
|
| 604 |
+
|
| 605 |
+
def my_handler(_):
|
| 606 |
+
return "foobar"
|
| 607 |
+
|
| 608 |
+
assert '"foobar"' == ujson.ujson_dumps(
|
| 609 |
+
_TestObject("foo"), default_handler=my_handler
|
| 610 |
+
)
|
| 611 |
+
|
| 612 |
+
def my_handler_raises(_):
|
| 613 |
+
raise TypeError("I raise for anything")
|
| 614 |
+
|
| 615 |
+
with pytest.raises(TypeError, match="I raise for anything"):
|
| 616 |
+
ujson.ujson_dumps(_TestObject("foo"), default_handler=my_handler_raises)
|
| 617 |
+
|
| 618 |
+
def my_int_handler(_):
|
| 619 |
+
return 42
|
| 620 |
+
|
| 621 |
+
assert (
|
| 622 |
+
ujson.ujson_loads(
|
| 623 |
+
ujson.ujson_dumps(_TestObject("foo"), default_handler=my_int_handler)
|
| 624 |
+
)
|
| 625 |
+
== 42
|
| 626 |
+
)
|
| 627 |
+
|
| 628 |
+
def my_obj_handler(_):
|
| 629 |
+
return datetime.datetime(2013, 2, 3)
|
| 630 |
+
|
| 631 |
+
assert ujson.ujson_loads(
|
| 632 |
+
ujson.ujson_dumps(datetime.datetime(2013, 2, 3))
|
| 633 |
+
) == ujson.ujson_loads(
|
| 634 |
+
ujson.ujson_dumps(_TestObject("foo"), default_handler=my_obj_handler)
|
| 635 |
+
)
|
| 636 |
+
|
| 637 |
+
obj_list = [_TestObject("foo"), _TestObject("bar")]
|
| 638 |
+
assert json.loads(json.dumps(obj_list, default=str)) == ujson.ujson_loads(
|
| 639 |
+
ujson.ujson_dumps(obj_list, default_handler=str)
|
| 640 |
+
)
|
| 641 |
+
|
| 642 |
+
def test_encode_object(self):
|
| 643 |
+
class _TestObject:
|
| 644 |
+
def __init__(self, a, b, _c, d) -> None:
|
| 645 |
+
self.a = a
|
| 646 |
+
self.b = b
|
| 647 |
+
self._c = _c
|
| 648 |
+
self.d = d
|
| 649 |
+
|
| 650 |
+
def e(self):
|
| 651 |
+
return 5
|
| 652 |
+
|
| 653 |
+
# JSON keys should be all non-callable non-underscore attributes, see GH-42768
|
| 654 |
+
test_object = _TestObject(a=1, b=2, _c=3, d=4)
|
| 655 |
+
assert ujson.ujson_loads(ujson.ujson_dumps(test_object)) == {
|
| 656 |
+
"a": 1,
|
| 657 |
+
"b": 2,
|
| 658 |
+
"d": 4,
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
def test_ujson__name__(self):
|
| 662 |
+
# GH 52898
|
| 663 |
+
assert ujson.__name__ == "pandas._libs.json"
|
| 664 |
+
|
| 665 |
+
|
| 666 |
+
class TestNumpyJSONTests:
|
| 667 |
+
@pytest.mark.parametrize("bool_input", [True, False])
|
| 668 |
+
def test_bool(self, bool_input):
|
| 669 |
+
b = bool(bool_input)
|
| 670 |
+
assert ujson.ujson_loads(ujson.ujson_dumps(b)) == b
|
| 671 |
+
|
| 672 |
+
def test_bool_array(self):
|
| 673 |
+
bool_array = np.array(
|
| 674 |
+
[True, False, True, True, False, True, False, False], dtype=bool
|
| 675 |
+
)
|
| 676 |
+
output = np.array(ujson.ujson_loads(ujson.ujson_dumps(bool_array)), dtype=bool)
|
| 677 |
+
tm.assert_numpy_array_equal(bool_array, output)
|
| 678 |
+
|
| 679 |
+
def test_int(self, any_int_numpy_dtype):
|
| 680 |
+
klass = np.dtype(any_int_numpy_dtype).type
|
| 681 |
+
num = klass(1)
|
| 682 |
+
|
| 683 |
+
assert klass(ujson.ujson_loads(ujson.ujson_dumps(num))) == num
|
| 684 |
+
|
| 685 |
+
def test_int_array(self, any_int_numpy_dtype):
|
| 686 |
+
arr = np.arange(100, dtype=int)
|
| 687 |
+
arr_input = arr.astype(any_int_numpy_dtype)
|
| 688 |
+
|
| 689 |
+
arr_output = np.array(
|
| 690 |
+
ujson.ujson_loads(ujson.ujson_dumps(arr_input)), dtype=any_int_numpy_dtype
|
| 691 |
+
)
|
| 692 |
+
tm.assert_numpy_array_equal(arr_input, arr_output)
|
| 693 |
+
|
| 694 |
+
def test_int_max(self, any_int_numpy_dtype):
|
| 695 |
+
if any_int_numpy_dtype in ("int64", "uint64") and not IS64:
|
| 696 |
+
pytest.skip("Cannot test 64-bit integer on 32-bit platform")
|
| 697 |
+
|
| 698 |
+
klass = np.dtype(any_int_numpy_dtype).type
|
| 699 |
+
|
| 700 |
+
# uint64 max will always overflow,
|
| 701 |
+
# as it's encoded to signed.
|
| 702 |
+
if any_int_numpy_dtype == "uint64":
|
| 703 |
+
num = np.iinfo("int64").max
|
| 704 |
+
else:
|
| 705 |
+
num = np.iinfo(any_int_numpy_dtype).max
|
| 706 |
+
|
| 707 |
+
assert klass(ujson.ujson_loads(ujson.ujson_dumps(num))) == num
|
| 708 |
+
|
| 709 |
+
def test_float(self, float_numpy_dtype):
|
| 710 |
+
klass = np.dtype(float_numpy_dtype).type
|
| 711 |
+
num = klass(256.2013)
|
| 712 |
+
|
| 713 |
+
assert klass(ujson.ujson_loads(ujson.ujson_dumps(num))) == num
|
| 714 |
+
|
| 715 |
+
def test_float_array(self, float_numpy_dtype):
|
| 716 |
+
arr = np.arange(12.5, 185.72, 1.7322, dtype=float)
|
| 717 |
+
float_input = arr.astype(float_numpy_dtype)
|
| 718 |
+
|
| 719 |
+
float_output = np.array(
|
| 720 |
+
ujson.ujson_loads(ujson.ujson_dumps(float_input, double_precision=15)),
|
| 721 |
+
dtype=float_numpy_dtype,
|
| 722 |
+
)
|
| 723 |
+
tm.assert_almost_equal(float_input, float_output)
|
| 724 |
+
|
| 725 |
+
def test_float_max(self, float_numpy_dtype):
|
| 726 |
+
klass = np.dtype(float_numpy_dtype).type
|
| 727 |
+
num = klass(np.finfo(float_numpy_dtype).max / 10)
|
| 728 |
+
|
| 729 |
+
tm.assert_almost_equal(
|
| 730 |
+
klass(ujson.ujson_loads(ujson.ujson_dumps(num, double_precision=15))), num
|
| 731 |
+
)
|
| 732 |
+
|
| 733 |
+
def test_array_basic(self):
|
| 734 |
+
arr = np.arange(96)
|
| 735 |
+
arr = arr.reshape((2, 2, 2, 2, 3, 2))
|
| 736 |
+
|
| 737 |
+
tm.assert_numpy_array_equal(
|
| 738 |
+
np.array(ujson.ujson_loads(ujson.ujson_dumps(arr))), arr
|
| 739 |
+
)
|
| 740 |
+
|
| 741 |
+
@pytest.mark.parametrize("shape", [(10, 10), (5, 5, 4), (100, 1)])
|
| 742 |
+
def test_array_reshaped(self, shape):
|
| 743 |
+
arr = np.arange(100)
|
| 744 |
+
arr = arr.reshape(shape)
|
| 745 |
+
|
| 746 |
+
tm.assert_numpy_array_equal(
|
| 747 |
+
np.array(ujson.ujson_loads(ujson.ujson_dumps(arr))), arr
|
| 748 |
+
)
|
| 749 |
+
|
| 750 |
+
def test_array_list(self):
|
| 751 |
+
arr_list = [
|
| 752 |
+
"a",
|
| 753 |
+
[],
|
| 754 |
+
{},
|
| 755 |
+
{},
|
| 756 |
+
[],
|
| 757 |
+
42,
|
| 758 |
+
97.8,
|
| 759 |
+
["a", "b"],
|
| 760 |
+
{"key": "val"},
|
| 761 |
+
]
|
| 762 |
+
arr = np.array(arr_list, dtype=object)
|
| 763 |
+
result = np.array(ujson.ujson_loads(ujson.ujson_dumps(arr)), dtype=object)
|
| 764 |
+
tm.assert_numpy_array_equal(result, arr)
|
| 765 |
+
|
| 766 |
+
def test_array_float(self):
|
| 767 |
+
dtype = np.float32
|
| 768 |
+
|
| 769 |
+
arr = np.arange(100.202, 200.202, 1, dtype=dtype)
|
| 770 |
+
arr = arr.reshape((5, 5, 4))
|
| 771 |
+
|
| 772 |
+
arr_out = np.array(ujson.ujson_loads(ujson.ujson_dumps(arr)), dtype=dtype)
|
| 773 |
+
tm.assert_almost_equal(arr, arr_out)
|
| 774 |
+
|
| 775 |
+
def test_0d_array(self):
|
| 776 |
+
# gh-18878
|
| 777 |
+
msg = re.escape(
|
| 778 |
+
"array(1) (numpy-scalar) is not JSON serializable at the moment"
|
| 779 |
+
)
|
| 780 |
+
with pytest.raises(TypeError, match=msg):
|
| 781 |
+
ujson.ujson_dumps(np.array(1))
|
| 782 |
+
|
| 783 |
+
def test_array_long_double(self):
|
| 784 |
+
msg = re.compile(
|
| 785 |
+
"1234.5.* \\(numpy-scalar\\) is not JSON serializable at the moment"
|
| 786 |
+
)
|
| 787 |
+
with pytest.raises(TypeError, match=msg):
|
| 788 |
+
ujson.ujson_dumps(np.longdouble(1234.5))
|
| 789 |
+
|
| 790 |
+
|
| 791 |
+
class TestPandasJSONTests:
|
| 792 |
+
def test_dataframe(self, orient):
|
| 793 |
+
dtype = np.int64
|
| 794 |
+
|
| 795 |
+
df = DataFrame(
|
| 796 |
+
[[1, 2, 3], [4, 5, 6]],
|
| 797 |
+
index=["a", "b"],
|
| 798 |
+
columns=["x", "y", "z"],
|
| 799 |
+
dtype=dtype,
|
| 800 |
+
)
|
| 801 |
+
encode_kwargs = {} if orient is None else {"orient": orient}
|
| 802 |
+
assert (df.dtypes == dtype).all()
|
| 803 |
+
|
| 804 |
+
output = ujson.ujson_loads(ujson.ujson_dumps(df, **encode_kwargs))
|
| 805 |
+
assert (df.dtypes == dtype).all()
|
| 806 |
+
|
| 807 |
+
# Ensure proper DataFrame initialization.
|
| 808 |
+
if orient == "split":
|
| 809 |
+
dec = _clean_dict(output)
|
| 810 |
+
output = DataFrame(**dec)
|
| 811 |
+
else:
|
| 812 |
+
output = DataFrame(output)
|
| 813 |
+
|
| 814 |
+
# Corrections to enable DataFrame comparison.
|
| 815 |
+
if orient == "values":
|
| 816 |
+
df.columns = [0, 1, 2]
|
| 817 |
+
df.index = [0, 1]
|
| 818 |
+
elif orient == "records":
|
| 819 |
+
df.index = [0, 1]
|
| 820 |
+
elif orient == "index":
|
| 821 |
+
df = df.transpose()
|
| 822 |
+
|
| 823 |
+
assert (df.dtypes == dtype).all()
|
| 824 |
+
tm.assert_frame_equal(output, df)
|
| 825 |
+
|
| 826 |
+
def test_dataframe_nested(self, orient):
|
| 827 |
+
df = DataFrame(
|
| 828 |
+
[[1, 2, 3], [4, 5, 6]], index=["a", "b"], columns=["x", "y", "z"]
|
| 829 |
+
)
|
| 830 |
+
|
| 831 |
+
nested = {"df1": df, "df2": df.copy()}
|
| 832 |
+
kwargs = {} if orient is None else {"orient": orient}
|
| 833 |
+
|
| 834 |
+
exp = {
|
| 835 |
+
"df1": ujson.ujson_loads(ujson.ujson_dumps(df, **kwargs)),
|
| 836 |
+
"df2": ujson.ujson_loads(ujson.ujson_dumps(df, **kwargs)),
|
| 837 |
+
}
|
| 838 |
+
assert ujson.ujson_loads(ujson.ujson_dumps(nested, **kwargs)) == exp
|
| 839 |
+
|
| 840 |
+
def test_series(self, orient):
|
| 841 |
+
dtype = np.int64
|
| 842 |
+
s = Series(
|
| 843 |
+
[10, 20, 30, 40, 50, 60],
|
| 844 |
+
name="series",
|
| 845 |
+
index=[6, 7, 8, 9, 10, 15],
|
| 846 |
+
dtype=dtype,
|
| 847 |
+
).sort_values()
|
| 848 |
+
assert s.dtype == dtype
|
| 849 |
+
|
| 850 |
+
encode_kwargs = {} if orient is None else {"orient": orient}
|
| 851 |
+
|
| 852 |
+
output = ujson.ujson_loads(ujson.ujson_dumps(s, **encode_kwargs))
|
| 853 |
+
assert s.dtype == dtype
|
| 854 |
+
|
| 855 |
+
if orient == "split":
|
| 856 |
+
dec = _clean_dict(output)
|
| 857 |
+
output = Series(**dec)
|
| 858 |
+
else:
|
| 859 |
+
output = Series(output)
|
| 860 |
+
|
| 861 |
+
if orient in (None, "index"):
|
| 862 |
+
s.name = None
|
| 863 |
+
output = output.sort_values()
|
| 864 |
+
s.index = ["6", "7", "8", "9", "10", "15"]
|
| 865 |
+
elif orient in ("records", "values"):
|
| 866 |
+
s.name = None
|
| 867 |
+
s.index = [0, 1, 2, 3, 4, 5]
|
| 868 |
+
|
| 869 |
+
assert s.dtype == dtype
|
| 870 |
+
tm.assert_series_equal(output, s)
|
| 871 |
+
|
| 872 |
+
def test_series_nested(self, orient):
|
| 873 |
+
s = Series(
|
| 874 |
+
[10, 20, 30, 40, 50, 60], name="series", index=[6, 7, 8, 9, 10, 15]
|
| 875 |
+
).sort_values()
|
| 876 |
+
nested = {"s1": s, "s2": s.copy()}
|
| 877 |
+
kwargs = {} if orient is None else {"orient": orient}
|
| 878 |
+
|
| 879 |
+
exp = {
|
| 880 |
+
"s1": ujson.ujson_loads(ujson.ujson_dumps(s, **kwargs)),
|
| 881 |
+
"s2": ujson.ujson_loads(ujson.ujson_dumps(s, **kwargs)),
|
| 882 |
+
}
|
| 883 |
+
assert ujson.ujson_loads(ujson.ujson_dumps(nested, **kwargs)) == exp
|
| 884 |
+
|
| 885 |
+
def test_index(self):
|
| 886 |
+
i = Index([23, 45, 18, 98, 43, 11], name="index")
|
| 887 |
+
|
| 888 |
+
# Column indexed.
|
| 889 |
+
output = Index(ujson.ujson_loads(ujson.ujson_dumps(i)), name="index")
|
| 890 |
+
tm.assert_index_equal(i, output)
|
| 891 |
+
|
| 892 |
+
dec = _clean_dict(ujson.ujson_loads(ujson.ujson_dumps(i, orient="split")))
|
| 893 |
+
output = Index(**dec)
|
| 894 |
+
|
| 895 |
+
tm.assert_index_equal(i, output)
|
| 896 |
+
assert i.name == output.name
|
| 897 |
+
|
| 898 |
+
tm.assert_index_equal(i, output)
|
| 899 |
+
assert i.name == output.name
|
| 900 |
+
|
| 901 |
+
output = Index(
|
| 902 |
+
ujson.ujson_loads(ujson.ujson_dumps(i, orient="values")), name="index"
|
| 903 |
+
)
|
| 904 |
+
tm.assert_index_equal(i, output)
|
| 905 |
+
|
| 906 |
+
output = Index(
|
| 907 |
+
ujson.ujson_loads(ujson.ujson_dumps(i, orient="records")), name="index"
|
| 908 |
+
)
|
| 909 |
+
tm.assert_index_equal(i, output)
|
| 910 |
+
|
| 911 |
+
output = Index(
|
| 912 |
+
ujson.ujson_loads(ujson.ujson_dumps(i, orient="index")), name="index"
|
| 913 |
+
)
|
| 914 |
+
tm.assert_index_equal(i, output)
|
| 915 |
+
|
| 916 |
+
def test_datetime_index(self):
|
| 917 |
+
date_unit = "ns"
|
| 918 |
+
|
| 919 |
+
# freq doesn't round-trip
|
| 920 |
+
rng = DatetimeIndex(
|
| 921 |
+
list(date_range("1/1/2000", periods=20, unit="ns")), freq=None
|
| 922 |
+
)
|
| 923 |
+
encoded = ujson.ujson_dumps(rng, date_unit=date_unit)
|
| 924 |
+
|
| 925 |
+
decoded = DatetimeIndex(np.array(ujson.ujson_loads(encoded)))
|
| 926 |
+
tm.assert_index_equal(rng, decoded)
|
| 927 |
+
|
| 928 |
+
ts = Series(np.random.default_rng(2).standard_normal(len(rng)), index=rng)
|
| 929 |
+
decoded = Series(ujson.ujson_loads(ujson.ujson_dumps(ts, date_unit=date_unit)))
|
| 930 |
+
|
| 931 |
+
idx_values = decoded.index.values.astype(np.int64)
|
| 932 |
+
decoded.index = DatetimeIndex(idx_values)
|
| 933 |
+
tm.assert_series_equal(ts, decoded)
|
| 934 |
+
|
| 935 |
+
@pytest.mark.parametrize(
|
| 936 |
+
"invalid_arr",
|
| 937 |
+
[
|
| 938 |
+
"[31337,]", # Trailing comma.
|
| 939 |
+
"[,31337]", # Leading comma.
|
| 940 |
+
"[]]", # Unmatched bracket.
|
| 941 |
+
"[,]", # Only comma.
|
| 942 |
+
],
|
| 943 |
+
)
|
| 944 |
+
def test_decode_invalid_array(self, invalid_arr):
|
| 945 |
+
msg = (
|
| 946 |
+
"Expected object or value|Trailing data|"
|
| 947 |
+
"Unexpected character found when decoding array value"
|
| 948 |
+
)
|
| 949 |
+
with pytest.raises(ValueError, match=msg):
|
| 950 |
+
ujson.ujson_loads(invalid_arr)
|
| 951 |
+
|
| 952 |
+
@pytest.mark.parametrize("arr", [[], [31337]])
|
| 953 |
+
def test_decode_array(self, arr):
|
| 954 |
+
assert arr == ujson.ujson_loads(str(arr))
|
| 955 |
+
|
| 956 |
+
@pytest.mark.parametrize("extreme_num", [9223372036854775807, -9223372036854775808])
|
| 957 |
+
def test_decode_extreme_numbers(self, extreme_num):
|
| 958 |
+
assert extreme_num == ujson.ujson_loads(str(extreme_num))
|
| 959 |
+
|
| 960 |
+
@pytest.mark.parametrize("too_extreme_num", [f"{2**64}", f"{-(2**63) - 1}"])
|
| 961 |
+
def test_decode_too_extreme_numbers(self, too_extreme_num):
|
| 962 |
+
with pytest.raises(
|
| 963 |
+
ValueError,
|
| 964 |
+
match="Value is too big|Value is too small",
|
| 965 |
+
):
|
| 966 |
+
ujson.ujson_loads(too_extreme_num)
|
| 967 |
+
|
| 968 |
+
def test_decode_with_trailing_whitespaces(self):
|
| 969 |
+
assert {} == ujson.ujson_loads("{}\n\t ")
|
| 970 |
+
|
| 971 |
+
def test_decode_with_trailing_non_whitespaces(self):
|
| 972 |
+
with pytest.raises(ValueError, match="Trailing data"):
|
| 973 |
+
ujson.ujson_loads("{}\n\t a")
|
| 974 |
+
|
| 975 |
+
@pytest.mark.parametrize("value", [f"{2**64}", f"{-(2**63) - 1}"])
|
| 976 |
+
def test_decode_array_with_big_int(self, value):
|
| 977 |
+
with pytest.raises(
|
| 978 |
+
ValueError,
|
| 979 |
+
match="Value is too big|Value is too small",
|
| 980 |
+
):
|
| 981 |
+
ujson.ujson_loads(value)
|
| 982 |
+
|
| 983 |
+
@pytest.mark.parametrize(
|
| 984 |
+
"float_number",
|
| 985 |
+
[
|
| 986 |
+
1.1234567893,
|
| 987 |
+
1.234567893,
|
| 988 |
+
1.34567893,
|
| 989 |
+
1.4567893,
|
| 990 |
+
1.567893,
|
| 991 |
+
1.67893,
|
| 992 |
+
1.7893,
|
| 993 |
+
1.893,
|
| 994 |
+
1.3,
|
| 995 |
+
],
|
| 996 |
+
)
|
| 997 |
+
@pytest.mark.parametrize("sign", [-1, 1])
|
| 998 |
+
def test_decode_floating_point(self, sign, float_number):
|
| 999 |
+
float_number *= sign
|
| 1000 |
+
tm.assert_almost_equal(
|
| 1001 |
+
float_number, ujson.ujson_loads(str(float_number)), rtol=1e-15
|
| 1002 |
+
)
|
| 1003 |
+
|
| 1004 |
+
def test_encode_big_set(self):
|
| 1005 |
+
s = set(range(100000))
|
| 1006 |
+
# Make sure no Exception is raised.
|
| 1007 |
+
ujson.ujson_dumps(s)
|
| 1008 |
+
|
| 1009 |
+
def test_encode_empty_set(self):
|
| 1010 |
+
assert "[]" == ujson.ujson_dumps(set())
|
| 1011 |
+
|
| 1012 |
+
def test_encode_set(self):
|
| 1013 |
+
s = {1, 2, 3, 4, 5, 6, 7, 8, 9}
|
| 1014 |
+
enc = ujson.ujson_dumps(s)
|
| 1015 |
+
dec = ujson.ujson_loads(enc)
|
| 1016 |
+
|
| 1017 |
+
for v in dec:
|
| 1018 |
+
assert v in s
|
| 1019 |
+
|
| 1020 |
+
@pytest.mark.parametrize(
|
| 1021 |
+
"td",
|
| 1022 |
+
[
|
| 1023 |
+
Timedelta(days=366),
|
| 1024 |
+
Timedelta(days=-1),
|
| 1025 |
+
Timedelta(hours=13, minutes=5, seconds=5),
|
| 1026 |
+
Timedelta(hours=13, minutes=20, seconds=30),
|
| 1027 |
+
Timedelta(days=-1, nanoseconds=5),
|
| 1028 |
+
Timedelta(nanoseconds=1),
|
| 1029 |
+
Timedelta(microseconds=1, nanoseconds=1),
|
| 1030 |
+
Timedelta(milliseconds=1, microseconds=1, nanoseconds=1),
|
| 1031 |
+
Timedelta(milliseconds=999, microseconds=999, nanoseconds=999),
|
| 1032 |
+
],
|
| 1033 |
+
)
|
| 1034 |
+
def test_encode_timedelta_iso(self, td):
|
| 1035 |
+
# GH 28256
|
| 1036 |
+
result = ujson.ujson_dumps(td, iso_dates=True)
|
| 1037 |
+
expected = f'"{td.isoformat()}"'
|
| 1038 |
+
|
| 1039 |
+
assert result == expected
|
| 1040 |
+
|
| 1041 |
+
def test_encode_periodindex(self):
|
| 1042 |
+
# GH 46683
|
| 1043 |
+
p = PeriodIndex(["2022-04-06", "2022-04-07"], freq="D")
|
| 1044 |
+
df = DataFrame(index=p)
|
| 1045 |
+
assert df.to_json() == "{}"
|
.venv/lib/python3.13/site-packages/pandas/tests/io/parser/__init__.py
ADDED
|
File without changes
|
.venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/__init__.py
ADDED
|
File without changes
|
.venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_chunksize.py
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tests that work on both the Python and C engines but do not have a
|
| 3 |
+
specific classification into the other test modules.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from io import StringIO
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import pytest
|
| 10 |
+
|
| 11 |
+
from pandas._libs import parsers as libparsers
|
| 12 |
+
from pandas.errors import DtypeWarning
|
| 13 |
+
|
| 14 |
+
from pandas import (
|
| 15 |
+
DataFrame,
|
| 16 |
+
concat,
|
| 17 |
+
)
|
| 18 |
+
import pandas._testing as tm
|
| 19 |
+
|
| 20 |
+
pytestmark = pytest.mark.filterwarnings(
|
| 21 |
+
"ignore:Passing a BlockManager to DataFrame:DeprecationWarning"
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@pytest.mark.parametrize("index_col", [0, "index"])
|
| 26 |
+
def test_read_chunksize_with_index(all_parsers, index_col):
|
| 27 |
+
parser = all_parsers
|
| 28 |
+
data = """index,A,B,C,D
|
| 29 |
+
foo,2,3,4,5
|
| 30 |
+
bar,7,8,9,10
|
| 31 |
+
baz,12,13,14,15
|
| 32 |
+
qux,12,13,14,15
|
| 33 |
+
foo2,12,13,14,15
|
| 34 |
+
bar2,12,13,14,15
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
expected = DataFrame(
|
| 38 |
+
[
|
| 39 |
+
["foo", 2, 3, 4, 5],
|
| 40 |
+
["bar", 7, 8, 9, 10],
|
| 41 |
+
["baz", 12, 13, 14, 15],
|
| 42 |
+
["qux", 12, 13, 14, 15],
|
| 43 |
+
["foo2", 12, 13, 14, 15],
|
| 44 |
+
["bar2", 12, 13, 14, 15],
|
| 45 |
+
],
|
| 46 |
+
columns=["index", "A", "B", "C", "D"],
|
| 47 |
+
)
|
| 48 |
+
expected = expected.set_index("index")
|
| 49 |
+
|
| 50 |
+
if parser.engine == "pyarrow":
|
| 51 |
+
msg = "The 'chunksize' option is not supported with the 'pyarrow' engine"
|
| 52 |
+
with pytest.raises(ValueError, match=msg):
|
| 53 |
+
with parser.read_csv(StringIO(data), index_col=0, chunksize=2) as reader:
|
| 54 |
+
list(reader)
|
| 55 |
+
return
|
| 56 |
+
|
| 57 |
+
with parser.read_csv(StringIO(data), index_col=0, chunksize=2) as reader:
|
| 58 |
+
chunks = list(reader)
|
| 59 |
+
tm.assert_frame_equal(chunks[0], expected[:2])
|
| 60 |
+
tm.assert_frame_equal(chunks[1], expected[2:4])
|
| 61 |
+
tm.assert_frame_equal(chunks[2], expected[4:])
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@pytest.mark.parametrize("chunksize", [1.3, "foo", 0])
|
| 65 |
+
def test_read_chunksize_bad(all_parsers, chunksize):
|
| 66 |
+
data = """index,A,B,C,D
|
| 67 |
+
foo,2,3,4,5
|
| 68 |
+
bar,7,8,9,10
|
| 69 |
+
baz,12,13,14,15
|
| 70 |
+
qux,12,13,14,15
|
| 71 |
+
foo2,12,13,14,15
|
| 72 |
+
bar2,12,13,14,15
|
| 73 |
+
"""
|
| 74 |
+
parser = all_parsers
|
| 75 |
+
msg = r"'chunksize' must be an integer >=1"
|
| 76 |
+
if parser.engine == "pyarrow":
|
| 77 |
+
msg = "The 'chunksize' option is not supported with the 'pyarrow' engine"
|
| 78 |
+
|
| 79 |
+
with pytest.raises(ValueError, match=msg):
|
| 80 |
+
with parser.read_csv(StringIO(data), chunksize=chunksize) as _:
|
| 81 |
+
pass
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@pytest.mark.parametrize("chunksize", [2, 8])
|
| 85 |
+
def test_read_chunksize_and_nrows(all_parsers, chunksize):
|
| 86 |
+
# see gh-15755
|
| 87 |
+
data = """index,A,B,C,D
|
| 88 |
+
foo,2,3,4,5
|
| 89 |
+
bar,7,8,9,10
|
| 90 |
+
baz,12,13,14,15
|
| 91 |
+
qux,12,13,14,15
|
| 92 |
+
foo2,12,13,14,15
|
| 93 |
+
bar2,12,13,14,15
|
| 94 |
+
"""
|
| 95 |
+
parser = all_parsers
|
| 96 |
+
kwargs = {"index_col": 0, "nrows": 5}
|
| 97 |
+
|
| 98 |
+
if parser.engine == "pyarrow":
|
| 99 |
+
msg = "The 'nrows' option is not supported with the 'pyarrow' engine"
|
| 100 |
+
with pytest.raises(ValueError, match=msg):
|
| 101 |
+
parser.read_csv(StringIO(data), **kwargs)
|
| 102 |
+
return
|
| 103 |
+
|
| 104 |
+
expected = parser.read_csv(StringIO(data), **kwargs)
|
| 105 |
+
with parser.read_csv(StringIO(data), chunksize=chunksize, **kwargs) as reader:
|
| 106 |
+
tm.assert_frame_equal(concat(reader), expected)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def test_read_chunksize_and_nrows_changing_size(all_parsers):
|
| 110 |
+
data = """index,A,B,C,D
|
| 111 |
+
foo,2,3,4,5
|
| 112 |
+
bar,7,8,9,10
|
| 113 |
+
baz,12,13,14,15
|
| 114 |
+
qux,12,13,14,15
|
| 115 |
+
foo2,12,13,14,15
|
| 116 |
+
bar2,12,13,14,15
|
| 117 |
+
"""
|
| 118 |
+
parser = all_parsers
|
| 119 |
+
kwargs = {"index_col": 0, "nrows": 5}
|
| 120 |
+
|
| 121 |
+
if parser.engine == "pyarrow":
|
| 122 |
+
msg = "The 'nrows' option is not supported with the 'pyarrow' engine"
|
| 123 |
+
with pytest.raises(ValueError, match=msg):
|
| 124 |
+
parser.read_csv(StringIO(data), **kwargs)
|
| 125 |
+
return
|
| 126 |
+
|
| 127 |
+
expected = parser.read_csv(StringIO(data), **kwargs)
|
| 128 |
+
with parser.read_csv(StringIO(data), chunksize=8, **kwargs) as reader:
|
| 129 |
+
tm.assert_frame_equal(reader.get_chunk(size=2), expected.iloc[:2])
|
| 130 |
+
tm.assert_frame_equal(reader.get_chunk(size=4), expected.iloc[2:5])
|
| 131 |
+
|
| 132 |
+
with pytest.raises(StopIteration, match="^$"):
|
| 133 |
+
reader.get_chunk(size=3)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def test_get_chunk_passed_chunksize(all_parsers):
|
| 137 |
+
parser = all_parsers
|
| 138 |
+
data = """A,B,C
|
| 139 |
+
1,2,3
|
| 140 |
+
4,5,6
|
| 141 |
+
7,8,9
|
| 142 |
+
1,2,3"""
|
| 143 |
+
|
| 144 |
+
if parser.engine == "pyarrow":
|
| 145 |
+
msg = "The 'chunksize' option is not supported with the 'pyarrow' engine"
|
| 146 |
+
with pytest.raises(ValueError, match=msg):
|
| 147 |
+
with parser.read_csv(StringIO(data), chunksize=2) as reader:
|
| 148 |
+
reader.get_chunk()
|
| 149 |
+
return
|
| 150 |
+
|
| 151 |
+
with parser.read_csv(StringIO(data), chunksize=2) as reader:
|
| 152 |
+
result = reader.get_chunk()
|
| 153 |
+
|
| 154 |
+
expected = DataFrame([[1, 2, 3], [4, 5, 6]], columns=["A", "B", "C"])
|
| 155 |
+
tm.assert_frame_equal(result, expected)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
@pytest.mark.parametrize("kwargs", [{}, {"index_col": 0}])
|
| 159 |
+
def test_read_chunksize_compat(all_parsers, kwargs):
|
| 160 |
+
# see gh-12185
|
| 161 |
+
data = """index,A,B,C,D
|
| 162 |
+
foo,2,3,4,5
|
| 163 |
+
bar,7,8,9,10
|
| 164 |
+
baz,12,13,14,15
|
| 165 |
+
qux,12,13,14,15
|
| 166 |
+
foo2,12,13,14,15
|
| 167 |
+
bar2,12,13,14,15
|
| 168 |
+
"""
|
| 169 |
+
parser = all_parsers
|
| 170 |
+
result = parser.read_csv(StringIO(data), **kwargs)
|
| 171 |
+
|
| 172 |
+
if parser.engine == "pyarrow":
|
| 173 |
+
msg = "The 'chunksize' option is not supported with the 'pyarrow' engine"
|
| 174 |
+
with pytest.raises(ValueError, match=msg):
|
| 175 |
+
with parser.read_csv(StringIO(data), chunksize=2, **kwargs) as reader:
|
| 176 |
+
concat(reader)
|
| 177 |
+
return
|
| 178 |
+
|
| 179 |
+
with parser.read_csv(StringIO(data), chunksize=2, **kwargs) as reader:
|
| 180 |
+
via_reader = concat(reader)
|
| 181 |
+
tm.assert_frame_equal(via_reader, result)
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def test_read_chunksize_jagged_names(all_parsers):
|
| 185 |
+
# see gh-23509
|
| 186 |
+
parser = all_parsers
|
| 187 |
+
data = "\n".join(["0"] * 7 + [",".join(["0"] * 10)])
|
| 188 |
+
|
| 189 |
+
expected = DataFrame([[0] + [np.nan] * 9] * 7 + [[0] * 10])
|
| 190 |
+
|
| 191 |
+
if parser.engine == "pyarrow":
|
| 192 |
+
msg = "The 'chunksize' option is not supported with the 'pyarrow' engine"
|
| 193 |
+
with pytest.raises(ValueError, match=msg):
|
| 194 |
+
with parser.read_csv(
|
| 195 |
+
StringIO(data), names=range(10), chunksize=4
|
| 196 |
+
) as reader:
|
| 197 |
+
concat(reader)
|
| 198 |
+
return
|
| 199 |
+
|
| 200 |
+
with parser.read_csv(StringIO(data), names=range(10), chunksize=4) as reader:
|
| 201 |
+
result = concat(reader)
|
| 202 |
+
tm.assert_frame_equal(result, expected)
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def test_chunk_begins_with_newline_whitespace(all_parsers):
|
| 206 |
+
# see gh-10022
|
| 207 |
+
parser = all_parsers
|
| 208 |
+
data = "\n hello\nworld\n"
|
| 209 |
+
|
| 210 |
+
result = parser.read_csv(StringIO(data), header=None)
|
| 211 |
+
expected = DataFrame([" hello", "world"])
|
| 212 |
+
tm.assert_frame_equal(result, expected)
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
@pytest.mark.slow
|
| 216 |
+
def test_chunks_have_consistent_numerical_type(all_parsers, monkeypatch):
|
| 217 |
+
# mainly an issue with the C parser
|
| 218 |
+
heuristic = 2**3
|
| 219 |
+
parser = all_parsers
|
| 220 |
+
integers = [str(i) for i in range(heuristic - 1)]
|
| 221 |
+
data = "a\n" + "\n".join([*integers, "1.0", "2.0", *integers])
|
| 222 |
+
|
| 223 |
+
# Coercions should work without warnings.
|
| 224 |
+
with monkeypatch.context() as m:
|
| 225 |
+
m.setattr(libparsers, "DEFAULT_BUFFER_HEURISTIC", heuristic)
|
| 226 |
+
result = parser.read_csv(StringIO(data))
|
| 227 |
+
|
| 228 |
+
assert type(result.a[0]) is np.float64
|
| 229 |
+
assert result.a.dtype == float
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def test_warn_if_chunks_have_mismatched_type(
|
| 233 |
+
all_parsers, using_infer_string, monkeypatch
|
| 234 |
+
):
|
| 235 |
+
warning_type = None
|
| 236 |
+
parser = all_parsers
|
| 237 |
+
heuristic = 2**3
|
| 238 |
+
size = 10
|
| 239 |
+
|
| 240 |
+
# see gh-3866: if chunks are different types and can't
|
| 241 |
+
# be coerced using numerical types, then issue warning.
|
| 242 |
+
if parser.engine == "c" and parser.low_memory:
|
| 243 |
+
warning_type = DtypeWarning
|
| 244 |
+
# Use a size to hit warning path dictated by DEFAULT_BUFFER_HEURISTIC
|
| 245 |
+
# monkeypatched below
|
| 246 |
+
size = heuristic - 1
|
| 247 |
+
|
| 248 |
+
integers = [str(i) for i in range(size)]
|
| 249 |
+
data = "a\n" + "\n".join([*integers, "a", "b", *integers])
|
| 250 |
+
|
| 251 |
+
buf = StringIO(data)
|
| 252 |
+
|
| 253 |
+
if parser.engine == "pyarrow":
|
| 254 |
+
df = parser.read_csv(
|
| 255 |
+
buf,
|
| 256 |
+
)
|
| 257 |
+
else:
|
| 258 |
+
with monkeypatch.context() as m:
|
| 259 |
+
m.setattr(libparsers, "DEFAULT_BUFFER_HEURISTIC", heuristic)
|
| 260 |
+
df = parser.read_csv_check_warnings(
|
| 261 |
+
warning_type,
|
| 262 |
+
r"Columns \(0: a\) have mixed types. "
|
| 263 |
+
"Specify dtype option on import or set low_memory=False.",
|
| 264 |
+
buf,
|
| 265 |
+
)
|
| 266 |
+
if parser.engine == "c" and parser.low_memory:
|
| 267 |
+
assert df.a.dtype == object
|
| 268 |
+
elif using_infer_string:
|
| 269 |
+
assert df.a.dtype == "str"
|
| 270 |
+
else:
|
| 271 |
+
assert df.a.dtype == object
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
@pytest.mark.parametrize("iterator", [True, False])
|
| 275 |
+
def test_empty_with_nrows_chunksize(all_parsers, iterator):
|
| 276 |
+
# see gh-9535
|
| 277 |
+
parser = all_parsers
|
| 278 |
+
expected = DataFrame(columns=["foo", "bar"])
|
| 279 |
+
|
| 280 |
+
nrows = 10
|
| 281 |
+
data = StringIO("foo,bar\n")
|
| 282 |
+
|
| 283 |
+
if parser.engine == "pyarrow":
|
| 284 |
+
msg = (
|
| 285 |
+
"The '(nrows|chunksize)' option is not supported with the 'pyarrow' engine"
|
| 286 |
+
)
|
| 287 |
+
with pytest.raises(ValueError, match=msg):
|
| 288 |
+
if iterator:
|
| 289 |
+
with parser.read_csv(data, chunksize=nrows) as reader:
|
| 290 |
+
next(iter(reader))
|
| 291 |
+
else:
|
| 292 |
+
parser.read_csv(data, nrows=nrows)
|
| 293 |
+
return
|
| 294 |
+
|
| 295 |
+
if iterator:
|
| 296 |
+
with parser.read_csv(data, chunksize=nrows) as reader:
|
| 297 |
+
result = next(iter(reader))
|
| 298 |
+
else:
|
| 299 |
+
result = parser.read_csv(data, nrows=nrows)
|
| 300 |
+
|
| 301 |
+
tm.assert_frame_equal(result, expected)
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
def test_chunksize_with_usecols_second_block_shorter(all_parsers):
|
| 305 |
+
# GH#21211
|
| 306 |
+
parser = all_parsers
|
| 307 |
+
data = """1,2,3,4
|
| 308 |
+
5,6,7,8
|
| 309 |
+
9,10,11
|
| 310 |
+
"""
|
| 311 |
+
|
| 312 |
+
if parser.engine == "pyarrow":
|
| 313 |
+
msg = "The 'chunksize' option is not supported with the 'pyarrow' engine"
|
| 314 |
+
with pytest.raises(ValueError, match=msg):
|
| 315 |
+
parser.read_csv(
|
| 316 |
+
StringIO(data),
|
| 317 |
+
names=["a", "b"],
|
| 318 |
+
chunksize=2,
|
| 319 |
+
usecols=[0, 1],
|
| 320 |
+
header=None,
|
| 321 |
+
)
|
| 322 |
+
return
|
| 323 |
+
|
| 324 |
+
result_chunks = parser.read_csv(
|
| 325 |
+
StringIO(data),
|
| 326 |
+
names=["a", "b"],
|
| 327 |
+
chunksize=2,
|
| 328 |
+
usecols=[0, 1],
|
| 329 |
+
header=None,
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
+
expected_frames = [
|
| 333 |
+
DataFrame({"a": [1, 5], "b": [2, 6]}),
|
| 334 |
+
DataFrame({"a": [9], "b": [10]}, index=[2]),
|
| 335 |
+
]
|
| 336 |
+
|
| 337 |
+
for i, result in enumerate(result_chunks):
|
| 338 |
+
tm.assert_frame_equal(result, expected_frames[i])
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
def test_chunksize_second_block_shorter(all_parsers):
|
| 342 |
+
# GH#21211
|
| 343 |
+
parser = all_parsers
|
| 344 |
+
data = """a,b,c,d
|
| 345 |
+
1,2,3,4
|
| 346 |
+
5,6,7,8
|
| 347 |
+
9,10,11
|
| 348 |
+
"""
|
| 349 |
+
|
| 350 |
+
if parser.engine == "pyarrow":
|
| 351 |
+
msg = "The 'chunksize' option is not supported with the 'pyarrow' engine"
|
| 352 |
+
with pytest.raises(ValueError, match=msg):
|
| 353 |
+
parser.read_csv(StringIO(data), chunksize=2)
|
| 354 |
+
return
|
| 355 |
+
|
| 356 |
+
result_chunks = parser.read_csv(StringIO(data), chunksize=2)
|
| 357 |
+
|
| 358 |
+
expected_frames = [
|
| 359 |
+
DataFrame({"a": [1, 5], "b": [2, 6], "c": [3, 7], "d": [4, 8]}),
|
| 360 |
+
DataFrame({"a": [9], "b": [10], "c": [11], "d": [np.nan]}, index=[2]),
|
| 361 |
+
]
|
| 362 |
+
|
| 363 |
+
for i, result in enumerate(result_chunks):
|
| 364 |
+
tm.assert_frame_equal(result, expected_frames[i])
|
.venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_common_basic.py
ADDED
|
@@ -0,0 +1,868 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tests that work on both the Python and C engines but do not have a
|
| 3 |
+
specific classification into the other test modules.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from inspect import signature
|
| 8 |
+
from io import StringIO
|
| 9 |
+
import os
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
import pytest
|
| 14 |
+
|
| 15 |
+
from pandas._config import using_string_dtype
|
| 16 |
+
|
| 17 |
+
from pandas.compat import HAS_PYARROW
|
| 18 |
+
from pandas.errors import (
|
| 19 |
+
EmptyDataError,
|
| 20 |
+
ParserError,
|
| 21 |
+
ParserWarning,
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
from pandas import (
|
| 25 |
+
DataFrame,
|
| 26 |
+
Index,
|
| 27 |
+
compat,
|
| 28 |
+
)
|
| 29 |
+
import pandas._testing as tm
|
| 30 |
+
|
| 31 |
+
pytestmark = pytest.mark.filterwarnings(
|
| 32 |
+
"ignore:Passing a BlockManager to DataFrame:DeprecationWarning"
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
xfail_pyarrow = pytest.mark.usefixtures("pyarrow_xfail")
|
| 36 |
+
skip_pyarrow = pytest.mark.usefixtures("pyarrow_skip")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def test_read_csv_local(all_parsers, csv1):
|
| 40 |
+
prefix = "file:///" if compat.is_platform_windows() else "file://"
|
| 41 |
+
parser = all_parsers
|
| 42 |
+
|
| 43 |
+
fname = prefix + str(os.path.abspath(csv1))
|
| 44 |
+
result = parser.read_csv(fname, index_col=0, parse_dates=True)
|
| 45 |
+
|
| 46 |
+
expected = DataFrame(
|
| 47 |
+
[
|
| 48 |
+
[0.980269, 3.685731, -0.364216805298, -1.159738],
|
| 49 |
+
[1.047916, -0.041232, -0.16181208307, 0.212549],
|
| 50 |
+
[0.498581, 0.731168, -0.537677223318, 1.346270],
|
| 51 |
+
[1.120202, 1.567621, 0.00364077397681, 0.675253],
|
| 52 |
+
[-0.487094, 0.571455, -1.6116394093, 0.103469],
|
| 53 |
+
[0.836649, 0.246462, 0.588542635376, 1.062782],
|
| 54 |
+
[-0.157161, 1.340307, 1.1957779562, -1.097007],
|
| 55 |
+
],
|
| 56 |
+
columns=["A", "B", "C", "D"],
|
| 57 |
+
index=Index(
|
| 58 |
+
[
|
| 59 |
+
datetime(2000, 1, 3),
|
| 60 |
+
datetime(2000, 1, 4),
|
| 61 |
+
datetime(2000, 1, 5),
|
| 62 |
+
datetime(2000, 1, 6),
|
| 63 |
+
datetime(2000, 1, 7),
|
| 64 |
+
datetime(2000, 1, 10),
|
| 65 |
+
datetime(2000, 1, 11),
|
| 66 |
+
],
|
| 67 |
+
dtype="M8[us]",
|
| 68 |
+
name="index",
|
| 69 |
+
),
|
| 70 |
+
)
|
| 71 |
+
if parser.engine == "pyarrow":
|
| 72 |
+
expected.index = expected.index.astype("M8[s]")
|
| 73 |
+
tm.assert_frame_equal(result, expected)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
@pytest.mark.parametrize(
|
| 77 |
+
"number_csv, expected_number",
|
| 78 |
+
[
|
| 79 |
+
("2,334", 2334),
|
| 80 |
+
("-2,334", -2334),
|
| 81 |
+
("-2,334,", -2334),
|
| 82 |
+
# Multiple consecutive thousand separators are allowed in C engine,
|
| 83 |
+
# but it's not necessarily intended behavior and may change in the future.
|
| 84 |
+
("2,,,,,,,,,,,,,,,5", 25),
|
| 85 |
+
("2,,3,4,,,,,,,,,,,,5", 2345),
|
| 86 |
+
],
|
| 87 |
+
)
|
| 88 |
+
def test_1000_sep(all_parsers, number_csv, expected_number, request):
|
| 89 |
+
parser = all_parsers
|
| 90 |
+
data = f"""A|B|C
|
| 91 |
+
1|{number_csv}|5
|
| 92 |
+
10|13|10.
|
| 93 |
+
"""
|
| 94 |
+
expected = DataFrame({"A": [1, 10], "B": [expected_number, 13], "C": [5, 10.0]})
|
| 95 |
+
|
| 96 |
+
if parser.engine == "pyarrow":
|
| 97 |
+
msg = "The 'thousands' option is not supported with the 'pyarrow' engine"
|
| 98 |
+
with pytest.raises(ValueError, match=msg):
|
| 99 |
+
parser.read_csv(StringIO(data), sep="|", thousands=",")
|
| 100 |
+
return
|
| 101 |
+
elif parser.engine == "python" and ",," in number_csv:
|
| 102 |
+
mark = pytest.mark.xfail(
|
| 103 |
+
reason="Python engine doesn't allow consecutive thousands separators"
|
| 104 |
+
)
|
| 105 |
+
request.applymarker(mark)
|
| 106 |
+
|
| 107 |
+
result = parser.read_csv(StringIO(data), sep="|", thousands=",")
|
| 108 |
+
tm.assert_frame_equal(result, expected)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
@pytest.mark.parametrize("value", ["1 ,", ", 1", ",1"])
|
| 112 |
+
def test_1000_sep_not_stripped_after_whitespace(all_parsers, value):
|
| 113 |
+
parser = all_parsers
|
| 114 |
+
data = f"a\n{value}\n"
|
| 115 |
+
expected = DataFrame({"a": [value]})
|
| 116 |
+
|
| 117 |
+
if parser.engine == "pyarrow":
|
| 118 |
+
msg = "The 'thousands' option is not supported with the 'pyarrow' engine"
|
| 119 |
+
with pytest.raises(ValueError, match=msg):
|
| 120 |
+
parser.read_csv(StringIO(data), sep=";", thousands=",")
|
| 121 |
+
return
|
| 122 |
+
|
| 123 |
+
result = parser.read_csv(StringIO(data), sep=";", thousands=",")
|
| 124 |
+
tm.assert_frame_equal(result, expected)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
@xfail_pyarrow # ValueError: Found non-unique column index
|
| 128 |
+
def test_unnamed_columns(all_parsers):
|
| 129 |
+
data = """A,B,C,,
|
| 130 |
+
1,2,3,4,5
|
| 131 |
+
6,7,8,9,10
|
| 132 |
+
11,12,13,14,15
|
| 133 |
+
"""
|
| 134 |
+
parser = all_parsers
|
| 135 |
+
expected = DataFrame(
|
| 136 |
+
[[1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11, 12, 13, 14, 15]],
|
| 137 |
+
dtype=np.int64,
|
| 138 |
+
columns=["A", "B", "C", "Unnamed: 3", "Unnamed: 4"],
|
| 139 |
+
)
|
| 140 |
+
result = parser.read_csv(StringIO(data))
|
| 141 |
+
tm.assert_frame_equal(result, expected)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def test_csv_mixed_type(all_parsers):
|
| 145 |
+
data = """A,B,C
|
| 146 |
+
a,1,2
|
| 147 |
+
b,3,4
|
| 148 |
+
c,4,5
|
| 149 |
+
"""
|
| 150 |
+
parser = all_parsers
|
| 151 |
+
expected = DataFrame({"A": ["a", "b", "c"], "B": [1, 3, 4], "C": [2, 4, 5]})
|
| 152 |
+
result = parser.read_csv(StringIO(data))
|
| 153 |
+
tm.assert_frame_equal(result, expected)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def test_read_csv_low_memory_no_rows_with_index(all_parsers):
|
| 157 |
+
# see gh-21141
|
| 158 |
+
parser = all_parsers
|
| 159 |
+
|
| 160 |
+
if not parser.low_memory:
|
| 161 |
+
pytest.skip("This is a low-memory specific test")
|
| 162 |
+
|
| 163 |
+
data = """A,B,C
|
| 164 |
+
1,1,1,2
|
| 165 |
+
2,2,3,4
|
| 166 |
+
3,3,4,5
|
| 167 |
+
"""
|
| 168 |
+
|
| 169 |
+
if parser.engine == "pyarrow":
|
| 170 |
+
msg = "The 'nrows' option is not supported with the 'pyarrow' engine"
|
| 171 |
+
with pytest.raises(ValueError, match=msg):
|
| 172 |
+
parser.read_csv(StringIO(data), low_memory=True, index_col=0, nrows=0)
|
| 173 |
+
return
|
| 174 |
+
|
| 175 |
+
result = parser.read_csv(StringIO(data), low_memory=True, index_col=0, nrows=0)
|
| 176 |
+
expected = DataFrame(columns=["A", "B", "C"])
|
| 177 |
+
tm.assert_frame_equal(result, expected)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def test_read_csv_dataframe(all_parsers, csv1):
|
| 181 |
+
parser = all_parsers
|
| 182 |
+
result = parser.read_csv(csv1, index_col=0, parse_dates=True)
|
| 183 |
+
expected = DataFrame(
|
| 184 |
+
[
|
| 185 |
+
[0.980269, 3.685731, -0.364216805298, -1.159738],
|
| 186 |
+
[1.047916, -0.041232, -0.16181208307, 0.212549],
|
| 187 |
+
[0.498581, 0.731168, -0.537677223318, 1.346270],
|
| 188 |
+
[1.120202, 1.567621, 0.00364077397681, 0.675253],
|
| 189 |
+
[-0.487094, 0.571455, -1.6116394093, 0.103469],
|
| 190 |
+
[0.836649, 0.246462, 0.588542635376, 1.062782],
|
| 191 |
+
[-0.157161, 1.340307, 1.1957779562, -1.097007],
|
| 192 |
+
],
|
| 193 |
+
columns=["A", "B", "C", "D"],
|
| 194 |
+
index=Index(
|
| 195 |
+
[
|
| 196 |
+
datetime(2000, 1, 3),
|
| 197 |
+
datetime(2000, 1, 4),
|
| 198 |
+
datetime(2000, 1, 5),
|
| 199 |
+
datetime(2000, 1, 6),
|
| 200 |
+
datetime(2000, 1, 7),
|
| 201 |
+
datetime(2000, 1, 10),
|
| 202 |
+
datetime(2000, 1, 11),
|
| 203 |
+
],
|
| 204 |
+
dtype="M8[us]",
|
| 205 |
+
name="index",
|
| 206 |
+
),
|
| 207 |
+
)
|
| 208 |
+
if parser.engine == "pyarrow":
|
| 209 |
+
expected.index = expected.index.astype("M8[s]")
|
| 210 |
+
tm.assert_frame_equal(result, expected)
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
@pytest.mark.parametrize("nrows", [3, 3.0])
|
| 214 |
+
def test_read_nrows(all_parsers, nrows):
|
| 215 |
+
# see gh-10476
|
| 216 |
+
data = """index,A,B,C,D
|
| 217 |
+
foo,2,3,4,5
|
| 218 |
+
bar,7,8,9,10
|
| 219 |
+
baz,12,13,14,15
|
| 220 |
+
qux,12,13,14,15
|
| 221 |
+
foo2,12,13,14,15
|
| 222 |
+
bar2,12,13,14,15
|
| 223 |
+
"""
|
| 224 |
+
expected = DataFrame(
|
| 225 |
+
[["foo", 2, 3, 4, 5], ["bar", 7, 8, 9, 10], ["baz", 12, 13, 14, 15]],
|
| 226 |
+
columns=["index", "A", "B", "C", "D"],
|
| 227 |
+
)
|
| 228 |
+
parser = all_parsers
|
| 229 |
+
|
| 230 |
+
if parser.engine == "pyarrow":
|
| 231 |
+
msg = "The 'nrows' option is not supported with the 'pyarrow' engine"
|
| 232 |
+
with pytest.raises(ValueError, match=msg):
|
| 233 |
+
parser.read_csv(StringIO(data), nrows=nrows)
|
| 234 |
+
return
|
| 235 |
+
|
| 236 |
+
result = parser.read_csv(StringIO(data), nrows=nrows)
|
| 237 |
+
tm.assert_frame_equal(result, expected)
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
@pytest.mark.parametrize("nrows", [1.2, "foo", -1])
|
| 241 |
+
def test_read_nrows_bad(all_parsers, nrows):
|
| 242 |
+
data = """index,A,B,C,D
|
| 243 |
+
foo,2,3,4,5
|
| 244 |
+
bar,7,8,9,10
|
| 245 |
+
baz,12,13,14,15
|
| 246 |
+
qux,12,13,14,15
|
| 247 |
+
foo2,12,13,14,15
|
| 248 |
+
bar2,12,13,14,15
|
| 249 |
+
"""
|
| 250 |
+
msg = r"'nrows' must be an integer >=0"
|
| 251 |
+
parser = all_parsers
|
| 252 |
+
if parser.engine == "pyarrow":
|
| 253 |
+
msg = "The 'nrows' option is not supported with the 'pyarrow' engine"
|
| 254 |
+
|
| 255 |
+
with pytest.raises(ValueError, match=msg):
|
| 256 |
+
parser.read_csv(StringIO(data), nrows=nrows)
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def test_nrows_skipfooter_errors(all_parsers):
|
| 260 |
+
msg = "'skipfooter' not supported with 'nrows'"
|
| 261 |
+
data = "a\n1\n2\n3\n4\n5\n6"
|
| 262 |
+
parser = all_parsers
|
| 263 |
+
|
| 264 |
+
with pytest.raises(ValueError, match=msg):
|
| 265 |
+
parser.read_csv(StringIO(data), skipfooter=1, nrows=5)
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
@skip_pyarrow
|
| 269 |
+
def test_missing_trailing_delimiters(all_parsers):
|
| 270 |
+
parser = all_parsers
|
| 271 |
+
data = """A,B,C,D
|
| 272 |
+
1,2,3,4
|
| 273 |
+
1,3,3,
|
| 274 |
+
1,4,5"""
|
| 275 |
+
|
| 276 |
+
result = parser.read_csv(StringIO(data))
|
| 277 |
+
expected = DataFrame(
|
| 278 |
+
[[1, 2, 3, 4], [1, 3, 3, np.nan], [1, 4, 5, np.nan]],
|
| 279 |
+
columns=["A", "B", "C", "D"],
|
| 280 |
+
)
|
| 281 |
+
tm.assert_frame_equal(result, expected)
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def test_skip_initial_space(all_parsers):
|
| 285 |
+
data = (
|
| 286 |
+
'"09-Apr-2012", "01:10:18.300", 2456026.548822908, 12849, '
|
| 287 |
+
"1.00361, 1.12551, 330.65659, 0355626618.16711, 73.48821, "
|
| 288 |
+
"314.11625, 1917.09447, 179.71425, 80.000, 240.000, -350, "
|
| 289 |
+
"70.06056, 344.98370, 1, 1, -0.689265, -0.692787, "
|
| 290 |
+
"0.212036, 14.7674, 41.605, -9999.0, -9999.0, "
|
| 291 |
+
"-9999.0, -9999.0, -9999.0, -9999.0, 000, 012, 128"
|
| 292 |
+
)
|
| 293 |
+
parser = all_parsers
|
| 294 |
+
|
| 295 |
+
if parser.engine == "pyarrow":
|
| 296 |
+
msg = "The 'skipinitialspace' option is not supported with the 'pyarrow' engine"
|
| 297 |
+
with pytest.raises(ValueError, match=msg):
|
| 298 |
+
parser.read_csv(
|
| 299 |
+
StringIO(data),
|
| 300 |
+
names=list(range(33)),
|
| 301 |
+
header=None,
|
| 302 |
+
na_values=["-9999.0"],
|
| 303 |
+
skipinitialspace=True,
|
| 304 |
+
)
|
| 305 |
+
return
|
| 306 |
+
|
| 307 |
+
result = parser.read_csv(
|
| 308 |
+
StringIO(data),
|
| 309 |
+
names=list(range(33)),
|
| 310 |
+
header=None,
|
| 311 |
+
na_values=["-9999.0"],
|
| 312 |
+
skipinitialspace=True,
|
| 313 |
+
)
|
| 314 |
+
expected = DataFrame(
|
| 315 |
+
[
|
| 316 |
+
[
|
| 317 |
+
"09-Apr-2012",
|
| 318 |
+
"01:10:18.300",
|
| 319 |
+
2456026.548822908,
|
| 320 |
+
12849,
|
| 321 |
+
1.00361,
|
| 322 |
+
1.12551,
|
| 323 |
+
330.65659,
|
| 324 |
+
355626618.16711,
|
| 325 |
+
73.48821,
|
| 326 |
+
314.11625,
|
| 327 |
+
1917.09447,
|
| 328 |
+
179.71425,
|
| 329 |
+
80.0,
|
| 330 |
+
240.0,
|
| 331 |
+
-350,
|
| 332 |
+
70.06056,
|
| 333 |
+
344.9837,
|
| 334 |
+
1,
|
| 335 |
+
1,
|
| 336 |
+
-0.689265,
|
| 337 |
+
-0.692787,
|
| 338 |
+
0.212036,
|
| 339 |
+
14.7674,
|
| 340 |
+
41.605,
|
| 341 |
+
np.nan,
|
| 342 |
+
np.nan,
|
| 343 |
+
np.nan,
|
| 344 |
+
np.nan,
|
| 345 |
+
np.nan,
|
| 346 |
+
np.nan,
|
| 347 |
+
0,
|
| 348 |
+
12,
|
| 349 |
+
128,
|
| 350 |
+
]
|
| 351 |
+
]
|
| 352 |
+
)
|
| 353 |
+
tm.assert_frame_equal(result, expected)
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
@skip_pyarrow
|
| 357 |
+
def test_trailing_delimiters(all_parsers):
|
| 358 |
+
# see gh-2442
|
| 359 |
+
data = """A,B,C
|
| 360 |
+
1,2,3,
|
| 361 |
+
4,5,6,
|
| 362 |
+
7,8,9,"""
|
| 363 |
+
parser = all_parsers
|
| 364 |
+
result = parser.read_csv(StringIO(data), index_col=False)
|
| 365 |
+
|
| 366 |
+
expected = DataFrame({"A": [1, 4, 7], "B": [2, 5, 8], "C": [3, 6, 9]})
|
| 367 |
+
tm.assert_frame_equal(result, expected)
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
def test_escapechar(all_parsers):
|
| 371 |
+
# https://stackoverflow.com/questions/13824840/feature-request-for-
|
| 372 |
+
# pandas-read-csv
|
| 373 |
+
data = '''SEARCH_TERM,ACTUAL_URL
|
| 374 |
+
"bra tv board","http://www.ikea.com/se/sv/catalog/categories/departments/living_room/10475/?se%7cps%7cnonbranded%7cvardagsrum%7cgoogle%7ctv_bord"
|
| 375 |
+
"tv p\xc3\xa5 hjul","http://www.ikea.com/se/sv/catalog/categories/departments/living_room/10475/?se%7cps%7cnonbranded%7cvardagsrum%7cgoogle%7ctv_bord"
|
| 376 |
+
"SLAGBORD, \\"Bergslagen\\", IKEA:s 1700-tals series","http://www.ikea.com/se/sv/catalog/categories/departments/living_room/10475/?se%7cps%7cnonbranded%7cvardagsrum%7cgoogle%7ctv_bord"'''
|
| 377 |
+
|
| 378 |
+
parser = all_parsers
|
| 379 |
+
result = parser.read_csv(
|
| 380 |
+
StringIO(data), escapechar="\\", quotechar='"', encoding="utf-8"
|
| 381 |
+
)
|
| 382 |
+
|
| 383 |
+
assert result["SEARCH_TERM"][2] == 'SLAGBORD, "Bergslagen", IKEA:s 1700-tals series'
|
| 384 |
+
|
| 385 |
+
tm.assert_index_equal(result.columns, Index(["SEARCH_TERM", "ACTUAL_URL"]))
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
def test_ignore_leading_whitespace(all_parsers):
|
| 389 |
+
# see gh-3374, gh-6607
|
| 390 |
+
parser = all_parsers
|
| 391 |
+
data = " a b c\n 1 2 3\n 4 5 6\n 7 8 9"
|
| 392 |
+
|
| 393 |
+
if parser.engine == "pyarrow":
|
| 394 |
+
msg = "the 'pyarrow' engine does not support regex separators"
|
| 395 |
+
with pytest.raises(ValueError, match=msg):
|
| 396 |
+
parser.read_csv(StringIO(data), sep=r"\s+")
|
| 397 |
+
return
|
| 398 |
+
result = parser.read_csv(StringIO(data), sep=r"\s+")
|
| 399 |
+
|
| 400 |
+
expected = DataFrame({"a": [1, 4, 7], "b": [2, 5, 8], "c": [3, 6, 9]})
|
| 401 |
+
tm.assert_frame_equal(result, expected)
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
@skip_pyarrow
|
| 405 |
+
@pytest.mark.parametrize("usecols", [None, [0, 1], ["a", "b"]])
|
| 406 |
+
def test_uneven_lines_with_usecols(all_parsers, usecols):
|
| 407 |
+
# see gh-12203
|
| 408 |
+
parser = all_parsers
|
| 409 |
+
data = r"""a,b,c
|
| 410 |
+
0,1,2
|
| 411 |
+
3,4,5,6,7
|
| 412 |
+
8,9,10"""
|
| 413 |
+
|
| 414 |
+
if usecols is None:
|
| 415 |
+
# Make sure that an error is still raised
|
| 416 |
+
# when the "usecols" parameter is not provided.
|
| 417 |
+
msg = r"Expected \d+ fields in line \d+, saw \d+"
|
| 418 |
+
with pytest.raises(ParserError, match=msg):
|
| 419 |
+
parser.read_csv(StringIO(data))
|
| 420 |
+
else:
|
| 421 |
+
expected = DataFrame({"a": [0, 3, 8], "b": [1, 4, 9]})
|
| 422 |
+
|
| 423 |
+
result = parser.read_csv(StringIO(data), usecols=usecols)
|
| 424 |
+
tm.assert_frame_equal(result, expected)
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
@skip_pyarrow
|
| 428 |
+
@pytest.mark.parametrize(
|
| 429 |
+
"data,kwargs,expected",
|
| 430 |
+
[
|
| 431 |
+
# First, check to see that the response of parser when faced with no
|
| 432 |
+
# provided columns raises the correct error, with or without usecols.
|
| 433 |
+
("", {}, None),
|
| 434 |
+
("", {"usecols": ["X"]}, None),
|
| 435 |
+
(
|
| 436 |
+
",,",
|
| 437 |
+
{"names": ["Dummy", "X", "Dummy_2"], "usecols": ["X"]},
|
| 438 |
+
DataFrame(columns=["X"], index=[0], dtype=np.float64),
|
| 439 |
+
),
|
| 440 |
+
(
|
| 441 |
+
"",
|
| 442 |
+
{"names": ["Dummy", "X", "Dummy_2"], "usecols": ["X"]},
|
| 443 |
+
DataFrame(columns=["X"]),
|
| 444 |
+
),
|
| 445 |
+
],
|
| 446 |
+
)
|
| 447 |
+
def test_read_empty_with_usecols(all_parsers, data, kwargs, expected):
|
| 448 |
+
# see gh-12493
|
| 449 |
+
parser = all_parsers
|
| 450 |
+
|
| 451 |
+
if expected is None:
|
| 452 |
+
msg = "No columns to parse from file"
|
| 453 |
+
with pytest.raises(EmptyDataError, match=msg):
|
| 454 |
+
parser.read_csv(StringIO(data), **kwargs)
|
| 455 |
+
else:
|
| 456 |
+
result = parser.read_csv(StringIO(data), **kwargs)
|
| 457 |
+
tm.assert_frame_equal(result, expected)
|
| 458 |
+
|
| 459 |
+
|
| 460 |
+
@pytest.mark.parametrize(
|
| 461 |
+
"kwargs,expected_data",
|
| 462 |
+
[
|
| 463 |
+
# gh-8661, gh-8679: this should ignore six lines, including
|
| 464 |
+
# lines with trailing whitespace and blank lines.
|
| 465 |
+
(
|
| 466 |
+
{
|
| 467 |
+
"header": None,
|
| 468 |
+
"sep": r"\s+",
|
| 469 |
+
"skiprows": [0, 1, 2, 3, 5, 6],
|
| 470 |
+
"skip_blank_lines": True,
|
| 471 |
+
},
|
| 472 |
+
[[1.0, 2.0, 4.0], [5.1, np.nan, 10.0]],
|
| 473 |
+
),
|
| 474 |
+
# gh-8983: test skipping set of rows after a row with trailing spaces.
|
| 475 |
+
(
|
| 476 |
+
{
|
| 477 |
+
"sep": r"\s+",
|
| 478 |
+
"skiprows": [1, 2, 3, 5, 6],
|
| 479 |
+
"skip_blank_lines": True,
|
| 480 |
+
},
|
| 481 |
+
{"A": [1.0, 5.1], "B": [2.0, np.nan], "C": [4.0, 10]},
|
| 482 |
+
),
|
| 483 |
+
],
|
| 484 |
+
)
|
| 485 |
+
def test_trailing_spaces(all_parsers, kwargs, expected_data):
|
| 486 |
+
data = "A B C \nrandom line with trailing spaces \nskip\n1,2,3\n1,2.,4.\nrandom line with trailing tabs\t\t\t\n \n5.1,NaN,10.0\n" # noqa: E501
|
| 487 |
+
parser = all_parsers
|
| 488 |
+
|
| 489 |
+
if parser.engine == "pyarrow":
|
| 490 |
+
with pytest.raises(ValueError, match="the 'pyarrow' engine does not support"):
|
| 491 |
+
parser.read_csv(StringIO(data.replace(",", " ")), **kwargs)
|
| 492 |
+
return
|
| 493 |
+
expected = DataFrame(expected_data)
|
| 494 |
+
result = parser.read_csv(StringIO(data.replace(",", " ")), **kwargs)
|
| 495 |
+
tm.assert_frame_equal(result, expected)
|
| 496 |
+
|
| 497 |
+
|
| 498 |
+
def test_read_filepath_or_buffer(all_parsers):
|
| 499 |
+
# see gh-43366
|
| 500 |
+
parser = all_parsers
|
| 501 |
+
|
| 502 |
+
with pytest.raises(TypeError, match="Expected file path name or file-like"):
|
| 503 |
+
parser.read_csv(filepath_or_buffer=b"input")
|
| 504 |
+
|
| 505 |
+
|
| 506 |
+
def test_single_char_leading_whitespace(all_parsers):
|
| 507 |
+
# see gh-9710
|
| 508 |
+
parser = all_parsers
|
| 509 |
+
data = """\
|
| 510 |
+
MyColumn
|
| 511 |
+
a
|
| 512 |
+
b
|
| 513 |
+
a
|
| 514 |
+
b\n"""
|
| 515 |
+
|
| 516 |
+
if parser.engine == "pyarrow":
|
| 517 |
+
msg = "The 'skipinitialspace' option is not supported with the 'pyarrow' engine"
|
| 518 |
+
with pytest.raises(ValueError, match=msg):
|
| 519 |
+
parser.read_csv(
|
| 520 |
+
StringIO(data),
|
| 521 |
+
skipinitialspace=True,
|
| 522 |
+
)
|
| 523 |
+
return
|
| 524 |
+
expected = DataFrame({"MyColumn": list("abab")})
|
| 525 |
+
result = parser.read_csv(StringIO(data), skipinitialspace=True, sep=r"\s+")
|
| 526 |
+
tm.assert_frame_equal(result, expected)
|
| 527 |
+
|
| 528 |
+
|
| 529 |
+
@pytest.mark.parametrize(
|
| 530 |
+
"sep,skip_blank_lines,exp_data",
|
| 531 |
+
[
|
| 532 |
+
(",", True, [[1.0, 2.0, 4.0], [5.0, np.nan, 10.0], [-70.0, 0.4, 1.0]]),
|
| 533 |
+
(r"\s+", True, [[1.0, 2.0, 4.0], [5.0, np.nan, 10.0], [-70.0, 0.4, 1.0]]),
|
| 534 |
+
(
|
| 535 |
+
",",
|
| 536 |
+
False,
|
| 537 |
+
[
|
| 538 |
+
[1.0, 2.0, 4.0],
|
| 539 |
+
[np.nan, np.nan, np.nan],
|
| 540 |
+
[np.nan, np.nan, np.nan],
|
| 541 |
+
[5.0, np.nan, 10.0],
|
| 542 |
+
[np.nan, np.nan, np.nan],
|
| 543 |
+
[-70.0, 0.4, 1.0],
|
| 544 |
+
],
|
| 545 |
+
),
|
| 546 |
+
],
|
| 547 |
+
)
|
| 548 |
+
def test_empty_lines(all_parsers, sep, skip_blank_lines, exp_data, request):
|
| 549 |
+
parser = all_parsers
|
| 550 |
+
data = """\
|
| 551 |
+
A,B,C
|
| 552 |
+
1,2.,4.
|
| 553 |
+
|
| 554 |
+
|
| 555 |
+
5.,NaN,10.0
|
| 556 |
+
|
| 557 |
+
-70,.4,1
|
| 558 |
+
"""
|
| 559 |
+
|
| 560 |
+
if sep == r"\s+":
|
| 561 |
+
data = data.replace(",", " ")
|
| 562 |
+
|
| 563 |
+
if parser.engine == "pyarrow":
|
| 564 |
+
msg = "the 'pyarrow' engine does not support regex separators"
|
| 565 |
+
with pytest.raises(ValueError, match=msg):
|
| 566 |
+
parser.read_csv(
|
| 567 |
+
StringIO(data), sep=sep, skip_blank_lines=skip_blank_lines
|
| 568 |
+
)
|
| 569 |
+
return
|
| 570 |
+
|
| 571 |
+
result = parser.read_csv(StringIO(data), sep=sep, skip_blank_lines=skip_blank_lines)
|
| 572 |
+
expected = DataFrame(exp_data, columns=["A", "B", "C"])
|
| 573 |
+
tm.assert_frame_equal(result, expected)
|
| 574 |
+
|
| 575 |
+
|
| 576 |
+
@skip_pyarrow
|
| 577 |
+
def test_whitespace_lines(all_parsers):
|
| 578 |
+
parser = all_parsers
|
| 579 |
+
data = """
|
| 580 |
+
|
| 581 |
+
\t \t\t
|
| 582 |
+
\t
|
| 583 |
+
A,B,C
|
| 584 |
+
\t 1,2.,4.
|
| 585 |
+
5.,NaN,10.0
|
| 586 |
+
"""
|
| 587 |
+
expected = DataFrame([[1, 2.0, 4.0], [5.0, np.nan, 10.0]], columns=["A", "B", "C"])
|
| 588 |
+
result = parser.read_csv(StringIO(data))
|
| 589 |
+
tm.assert_frame_equal(result, expected)
|
| 590 |
+
|
| 591 |
+
|
| 592 |
+
@pytest.mark.parametrize(
|
| 593 |
+
"data,expected",
|
| 594 |
+
[
|
| 595 |
+
(
|
| 596 |
+
""" A B C D
|
| 597 |
+
a 1 2 3 4
|
| 598 |
+
b 1 2 3 4
|
| 599 |
+
c 1 2 3 4
|
| 600 |
+
""",
|
| 601 |
+
DataFrame(
|
| 602 |
+
[[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]],
|
| 603 |
+
columns=["A", "B", "C", "D"],
|
| 604 |
+
index=["a", "b", "c"],
|
| 605 |
+
),
|
| 606 |
+
),
|
| 607 |
+
(
|
| 608 |
+
" a b c\n1 2 3 \n4 5 6\n 7 8 9",
|
| 609 |
+
DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]], columns=["a", "b", "c"]),
|
| 610 |
+
),
|
| 611 |
+
],
|
| 612 |
+
)
|
| 613 |
+
def test_whitespace_regex_separator(all_parsers, data, expected):
|
| 614 |
+
# see gh-6607
|
| 615 |
+
parser = all_parsers
|
| 616 |
+
if parser.engine == "pyarrow":
|
| 617 |
+
msg = "the 'pyarrow' engine does not support regex separators"
|
| 618 |
+
with pytest.raises(ValueError, match=msg):
|
| 619 |
+
parser.read_csv(StringIO(data), sep=r"\s+")
|
| 620 |
+
return
|
| 621 |
+
|
| 622 |
+
result = parser.read_csv(StringIO(data), sep=r"\s+")
|
| 623 |
+
tm.assert_frame_equal(result, expected)
|
| 624 |
+
|
| 625 |
+
|
| 626 |
+
def test_sub_character(all_parsers, csv_dir_path):
|
| 627 |
+
# see gh-16893
|
| 628 |
+
filename = os.path.join(csv_dir_path, "sub_char.csv")
|
| 629 |
+
expected = DataFrame([[1, 2, 3]], columns=["a", "\x1ab", "c"])
|
| 630 |
+
|
| 631 |
+
parser = all_parsers
|
| 632 |
+
result = parser.read_csv(filename)
|
| 633 |
+
tm.assert_frame_equal(result, expected)
|
| 634 |
+
|
| 635 |
+
|
| 636 |
+
@pytest.mark.parametrize("filename", ["sé-es-vé.csv", "ru-sй.csv", "中文文件名.csv"])
|
| 637 |
+
def test_filename_with_special_chars(all_parsers, filename, tmp_path):
|
| 638 |
+
# see gh-15086.
|
| 639 |
+
parser = all_parsers
|
| 640 |
+
df = DataFrame({"a": [1, 2, 3]})
|
| 641 |
+
|
| 642 |
+
path = tmp_path / filename
|
| 643 |
+
df.to_csv(path, index=False)
|
| 644 |
+
|
| 645 |
+
result = parser.read_csv(path)
|
| 646 |
+
tm.assert_frame_equal(result, df)
|
| 647 |
+
|
| 648 |
+
|
| 649 |
+
def test_read_table_same_signature_as_read_csv(all_parsers):
|
| 650 |
+
# GH-34976
|
| 651 |
+
parser = all_parsers
|
| 652 |
+
|
| 653 |
+
table_sign = signature(parser.read_table)
|
| 654 |
+
csv_sign = signature(parser.read_csv)
|
| 655 |
+
|
| 656 |
+
assert table_sign.parameters.keys() == csv_sign.parameters.keys()
|
| 657 |
+
assert table_sign.return_annotation == csv_sign.return_annotation
|
| 658 |
+
|
| 659 |
+
for key, csv_param in csv_sign.parameters.items():
|
| 660 |
+
table_param = table_sign.parameters[key]
|
| 661 |
+
if key == "sep":
|
| 662 |
+
assert csv_param.default == ","
|
| 663 |
+
assert table_param.default == "\t"
|
| 664 |
+
assert table_param.annotation == csv_param.annotation
|
| 665 |
+
assert table_param.kind == csv_param.kind
|
| 666 |
+
continue
|
| 667 |
+
|
| 668 |
+
assert table_param == csv_param
|
| 669 |
+
|
| 670 |
+
|
| 671 |
+
def test_read_table_equivalency_to_read_csv(all_parsers):
|
| 672 |
+
# see gh-21948
|
| 673 |
+
# As of 0.25.0, read_table is undeprecated
|
| 674 |
+
parser = all_parsers
|
| 675 |
+
data = "a\tb\n1\t2\n3\t4"
|
| 676 |
+
expected = parser.read_csv(StringIO(data), sep="\t")
|
| 677 |
+
result = parser.read_table(StringIO(data))
|
| 678 |
+
tm.assert_frame_equal(result, expected)
|
| 679 |
+
|
| 680 |
+
|
| 681 |
+
@pytest.mark.parametrize("read_func", ["read_csv", "read_table"])
|
| 682 |
+
def test_read_csv_and_table_sys_setprofile(all_parsers, read_func):
|
| 683 |
+
# GH#41069
|
| 684 |
+
parser = all_parsers
|
| 685 |
+
data = "a b\n0 1"
|
| 686 |
+
|
| 687 |
+
sys.setprofile(lambda *a, **k: None)
|
| 688 |
+
result = getattr(parser, read_func)(StringIO(data))
|
| 689 |
+
sys.setprofile(None)
|
| 690 |
+
|
| 691 |
+
expected = DataFrame({"a b": ["0 1"]})
|
| 692 |
+
tm.assert_frame_equal(result, expected)
|
| 693 |
+
|
| 694 |
+
|
| 695 |
+
@skip_pyarrow
|
| 696 |
+
def test_first_row_bom(all_parsers):
|
| 697 |
+
# see gh-26545
|
| 698 |
+
parser = all_parsers
|
| 699 |
+
data = '''\ufeff"Head1"\t"Head2"\t"Head3"'''
|
| 700 |
+
|
| 701 |
+
result = parser.read_csv(StringIO(data), delimiter="\t")
|
| 702 |
+
expected = DataFrame(columns=["Head1", "Head2", "Head3"])
|
| 703 |
+
tm.assert_frame_equal(result, expected)
|
| 704 |
+
|
| 705 |
+
|
| 706 |
+
@skip_pyarrow
|
| 707 |
+
def test_first_row_bom_unquoted(all_parsers):
|
| 708 |
+
# see gh-36343
|
| 709 |
+
parser = all_parsers
|
| 710 |
+
data = """\ufeffHead1\tHead2\tHead3"""
|
| 711 |
+
|
| 712 |
+
result = parser.read_csv(StringIO(data), delimiter="\t")
|
| 713 |
+
expected = DataFrame(columns=["Head1", "Head2", "Head3"])
|
| 714 |
+
tm.assert_frame_equal(result, expected)
|
| 715 |
+
|
| 716 |
+
|
| 717 |
+
@pytest.mark.parametrize("nrows", range(1, 6))
|
| 718 |
+
def test_blank_lines_between_header_and_data_rows(all_parsers, nrows):
|
| 719 |
+
# GH 28071
|
| 720 |
+
ref = DataFrame(
|
| 721 |
+
[[np.nan, np.nan], [np.nan, np.nan], [1, 2], [np.nan, np.nan], [3, 4]],
|
| 722 |
+
columns=list("ab"),
|
| 723 |
+
)
|
| 724 |
+
csv = "\nheader\n\na,b\n\n\n1,2\n\n3,4"
|
| 725 |
+
parser = all_parsers
|
| 726 |
+
|
| 727 |
+
if parser.engine == "pyarrow":
|
| 728 |
+
msg = "The 'nrows' option is not supported with the 'pyarrow' engine"
|
| 729 |
+
with pytest.raises(ValueError, match=msg):
|
| 730 |
+
parser.read_csv(
|
| 731 |
+
StringIO(csv), header=3, nrows=nrows, skip_blank_lines=False
|
| 732 |
+
)
|
| 733 |
+
return
|
| 734 |
+
|
| 735 |
+
df = parser.read_csv(StringIO(csv), header=3, nrows=nrows, skip_blank_lines=False)
|
| 736 |
+
tm.assert_frame_equal(df, ref[:nrows])
|
| 737 |
+
|
| 738 |
+
|
| 739 |
+
@skip_pyarrow
|
| 740 |
+
def test_no_header_two_extra_columns(all_parsers):
|
| 741 |
+
# GH 26218
|
| 742 |
+
column_names = ["one", "two", "three"]
|
| 743 |
+
ref = DataFrame([["foo", "bar", "baz"]], columns=column_names)
|
| 744 |
+
stream = StringIO("foo,bar,baz,bam,blah")
|
| 745 |
+
parser = all_parsers
|
| 746 |
+
df = parser.read_csv_check_warnings(
|
| 747 |
+
ParserWarning,
|
| 748 |
+
"Length of header or names does not match length of data. "
|
| 749 |
+
"This leads to a loss of data with index_col=False.",
|
| 750 |
+
stream,
|
| 751 |
+
header=None,
|
| 752 |
+
names=column_names,
|
| 753 |
+
index_col=False,
|
| 754 |
+
)
|
| 755 |
+
tm.assert_frame_equal(df, ref)
|
| 756 |
+
|
| 757 |
+
|
| 758 |
+
def test_read_csv_names_not_accepting_sets(all_parsers):
|
| 759 |
+
# GH 34946
|
| 760 |
+
data = """\
|
| 761 |
+
1,2,3
|
| 762 |
+
4,5,6\n"""
|
| 763 |
+
parser = all_parsers
|
| 764 |
+
with pytest.raises(ValueError, match="Names should be an ordered collection."):
|
| 765 |
+
parser.read_csv(StringIO(data), names=set("QAZ"))
|
| 766 |
+
|
| 767 |
+
|
| 768 |
+
def test_read_csv_delimiter_and_sep_no_default(all_parsers):
|
| 769 |
+
# GH#39823
|
| 770 |
+
f = StringIO("a,b\n1,2")
|
| 771 |
+
parser = all_parsers
|
| 772 |
+
msg = "Specified a sep and a delimiter; you can only specify one."
|
| 773 |
+
with pytest.raises(ValueError, match=msg):
|
| 774 |
+
parser.read_csv(f, sep=" ", delimiter=".")
|
| 775 |
+
|
| 776 |
+
|
| 777 |
+
@pytest.mark.parametrize("kwargs", [{"delimiter": "\n"}, {"sep": "\n"}])
|
| 778 |
+
def test_read_csv_line_break_as_separator(kwargs, all_parsers):
|
| 779 |
+
# GH#43528
|
| 780 |
+
parser = all_parsers
|
| 781 |
+
data = """a,b,c
|
| 782 |
+
1,2,3
|
| 783 |
+
"""
|
| 784 |
+
msg = (
|
| 785 |
+
r"Specified \\n as separator or delimiter. This forces the python engine "
|
| 786 |
+
r"which does not accept a line terminator. Hence it is not allowed to use "
|
| 787 |
+
r"the line terminator as separator."
|
| 788 |
+
)
|
| 789 |
+
with pytest.raises(ValueError, match=msg):
|
| 790 |
+
parser.read_csv(StringIO(data), **kwargs)
|
| 791 |
+
|
| 792 |
+
|
| 793 |
+
@skip_pyarrow
|
| 794 |
+
def test_dict_keys_as_names(all_parsers):
|
| 795 |
+
# GH: 36928
|
| 796 |
+
data = "1,2"
|
| 797 |
+
|
| 798 |
+
keys = {"a": int, "b": int}.keys()
|
| 799 |
+
parser = all_parsers
|
| 800 |
+
|
| 801 |
+
result = parser.read_csv(StringIO(data), names=keys)
|
| 802 |
+
expected = DataFrame({"a": [1], "b": [2]})
|
| 803 |
+
tm.assert_frame_equal(result, expected)
|
| 804 |
+
|
| 805 |
+
|
| 806 |
+
@pytest.mark.xfail(using_string_dtype() and HAS_PYARROW, reason="TODO(infer_string)")
|
| 807 |
+
@xfail_pyarrow # UnicodeDecodeError: 'utf-8' codec can't decode byte 0xed in position 0
|
| 808 |
+
def test_encoding_surrogatepass(all_parsers, tmp_path):
|
| 809 |
+
# GH39017
|
| 810 |
+
parser = all_parsers
|
| 811 |
+
content = b"\xed\xbd\xbf"
|
| 812 |
+
decoded = content.decode("utf-8", errors="surrogatepass")
|
| 813 |
+
expected = DataFrame({decoded: [decoded]}, index=[decoded * 2])
|
| 814 |
+
expected.index.name = decoded * 2
|
| 815 |
+
|
| 816 |
+
path = tmp_path / "test_encoding.csv"
|
| 817 |
+
path.write_bytes(
|
| 818 |
+
content * 2 + b"," + content + b"\n" + content * 2 + b"," + content
|
| 819 |
+
)
|
| 820 |
+
df = parser.read_csv(path, encoding_errors="surrogatepass", index_col=0)
|
| 821 |
+
tm.assert_frame_equal(df, expected)
|
| 822 |
+
with pytest.raises(UnicodeDecodeError, match="'utf-8' codec can't decode byte"):
|
| 823 |
+
parser.read_csv(path)
|
| 824 |
+
|
| 825 |
+
|
| 826 |
+
def test_malformed_second_line(all_parsers):
|
| 827 |
+
# see GH14782
|
| 828 |
+
parser = all_parsers
|
| 829 |
+
data = "\na\nb\n"
|
| 830 |
+
result = parser.read_csv(StringIO(data), skip_blank_lines=False, header=1)
|
| 831 |
+
expected = DataFrame({"a": ["b"]})
|
| 832 |
+
tm.assert_frame_equal(result, expected)
|
| 833 |
+
|
| 834 |
+
|
| 835 |
+
@skip_pyarrow
|
| 836 |
+
def test_short_single_line(all_parsers):
|
| 837 |
+
# GH 47566
|
| 838 |
+
parser = all_parsers
|
| 839 |
+
columns = ["a", "b", "c"]
|
| 840 |
+
data = "1,2"
|
| 841 |
+
result = parser.read_csv(StringIO(data), header=None, names=columns)
|
| 842 |
+
expected = DataFrame({"a": [1], "b": [2], "c": [np.nan]})
|
| 843 |
+
tm.assert_frame_equal(result, expected)
|
| 844 |
+
|
| 845 |
+
|
| 846 |
+
@xfail_pyarrow # ValueError: Length mismatch: Expected axis has 2 elements
|
| 847 |
+
def test_short_multi_line(all_parsers):
|
| 848 |
+
# GH 47566
|
| 849 |
+
parser = all_parsers
|
| 850 |
+
columns = ["a", "b", "c"]
|
| 851 |
+
data = "1,2\n1,2"
|
| 852 |
+
result = parser.read_csv(StringIO(data), header=None, names=columns)
|
| 853 |
+
expected = DataFrame({"a": [1, 1], "b": [2, 2], "c": [np.nan, np.nan]})
|
| 854 |
+
tm.assert_frame_equal(result, expected)
|
| 855 |
+
|
| 856 |
+
|
| 857 |
+
def test_read_seek(all_parsers, tmp_path):
|
| 858 |
+
# GH48646
|
| 859 |
+
parser = all_parsers
|
| 860 |
+
prefix = "### DATA\n"
|
| 861 |
+
content = "nkey,value\ntables,rectangular\n"
|
| 862 |
+
path = tmp_path / "test_seek.csv"
|
| 863 |
+
path.write_text(prefix + content, encoding="utf-8")
|
| 864 |
+
with open(path, encoding="utf-8") as file:
|
| 865 |
+
file.readline()
|
| 866 |
+
actual = parser.read_csv(file)
|
| 867 |
+
expected = parser.read_csv(StringIO(content))
|
| 868 |
+
tm.assert_frame_equal(actual, expected)
|
.venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_data_list.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tests that work on both the Python and C engines but do not have a
|
| 3 |
+
specific classification into the other test modules.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import csv
|
| 7 |
+
from io import StringIO
|
| 8 |
+
|
| 9 |
+
import pytest
|
| 10 |
+
|
| 11 |
+
from pandas import DataFrame
|
| 12 |
+
import pandas._testing as tm
|
| 13 |
+
|
| 14 |
+
from pandas.io.parsers import TextParser
|
| 15 |
+
|
| 16 |
+
pytestmark = pytest.mark.filterwarnings(
|
| 17 |
+
"ignore:Passing a BlockManager to DataFrame:DeprecationWarning"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
xfail_pyarrow = pytest.mark.usefixtures("pyarrow_xfail")
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@xfail_pyarrow
|
| 24 |
+
def test_read_data_list(all_parsers):
|
| 25 |
+
parser = all_parsers
|
| 26 |
+
kwargs = {"index_col": 0}
|
| 27 |
+
data = "A,B,C\nfoo,1,2,3\nbar,4,5,6"
|
| 28 |
+
|
| 29 |
+
data_list = [["A", "B", "C"], ["foo", "1", "2", "3"], ["bar", "4", "5", "6"]]
|
| 30 |
+
expected = parser.read_csv(StringIO(data), **kwargs)
|
| 31 |
+
|
| 32 |
+
with TextParser(data_list, chunksize=2, **kwargs) as parser:
|
| 33 |
+
result = parser.read()
|
| 34 |
+
|
| 35 |
+
tm.assert_frame_equal(result, expected)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def test_reader_list(all_parsers):
|
| 39 |
+
data = """index,A,B,C,D
|
| 40 |
+
foo,2,3,4,5
|
| 41 |
+
bar,7,8,9,10
|
| 42 |
+
baz,12,13,14,15
|
| 43 |
+
qux,12,13,14,15
|
| 44 |
+
foo2,12,13,14,15
|
| 45 |
+
bar2,12,13,14,15
|
| 46 |
+
"""
|
| 47 |
+
parser = all_parsers
|
| 48 |
+
kwargs = {"index_col": 0}
|
| 49 |
+
|
| 50 |
+
lines = list(csv.reader(StringIO(data)))
|
| 51 |
+
with TextParser(lines, chunksize=2, **kwargs) as reader:
|
| 52 |
+
chunks = list(reader)
|
| 53 |
+
|
| 54 |
+
expected = parser.read_csv(StringIO(data), **kwargs)
|
| 55 |
+
|
| 56 |
+
tm.assert_frame_equal(chunks[0], expected[:2])
|
| 57 |
+
tm.assert_frame_equal(chunks[1], expected[2:4])
|
| 58 |
+
tm.assert_frame_equal(chunks[2], expected[4:])
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def test_reader_list_skiprows(all_parsers):
|
| 62 |
+
data = """index,A,B,C,D
|
| 63 |
+
foo,2,3,4,5
|
| 64 |
+
bar,7,8,9,10
|
| 65 |
+
baz,12,13,14,15
|
| 66 |
+
qux,12,13,14,15
|
| 67 |
+
foo2,12,13,14,15
|
| 68 |
+
bar2,12,13,14,15
|
| 69 |
+
"""
|
| 70 |
+
parser = all_parsers
|
| 71 |
+
kwargs = {"index_col": 0}
|
| 72 |
+
|
| 73 |
+
lines = list(csv.reader(StringIO(data)))
|
| 74 |
+
with TextParser(lines, chunksize=2, skiprows=[1], **kwargs) as reader:
|
| 75 |
+
chunks = list(reader)
|
| 76 |
+
|
| 77 |
+
expected = parser.read_csv(StringIO(data), **kwargs)
|
| 78 |
+
|
| 79 |
+
tm.assert_frame_equal(chunks[0], expected[1:3])
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def test_read_csv_parse_simple_list(all_parsers):
|
| 83 |
+
parser = all_parsers
|
| 84 |
+
data = """foo
|
| 85 |
+
bar baz
|
| 86 |
+
qux foo
|
| 87 |
+
foo
|
| 88 |
+
bar"""
|
| 89 |
+
|
| 90 |
+
result = parser.read_csv(StringIO(data), header=None)
|
| 91 |
+
expected = DataFrame(["foo", "bar baz", "qux foo", "foo", "bar"])
|
| 92 |
+
tm.assert_frame_equal(result, expected)
|
.venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_decimal.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tests that work on both the Python and C engines but do not have a
|
| 3 |
+
specific classification into the other test modules.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from io import StringIO
|
| 7 |
+
|
| 8 |
+
import pytest
|
| 9 |
+
|
| 10 |
+
from pandas import DataFrame
|
| 11 |
+
import pandas._testing as tm
|
| 12 |
+
|
| 13 |
+
pytestmark = pytest.mark.filterwarnings(
|
| 14 |
+
"ignore:Passing a BlockManager to DataFrame:DeprecationWarning"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@pytest.mark.parametrize(
|
| 19 |
+
"data,thousands,decimal",
|
| 20 |
+
[
|
| 21 |
+
(
|
| 22 |
+
"""A|B|C
|
| 23 |
+
1|2,334.01|5
|
| 24 |
+
10|13|10.
|
| 25 |
+
""",
|
| 26 |
+
",",
|
| 27 |
+
".",
|
| 28 |
+
),
|
| 29 |
+
(
|
| 30 |
+
"""A|B|C
|
| 31 |
+
1|2.334,01|5
|
| 32 |
+
10|13|10,
|
| 33 |
+
""",
|
| 34 |
+
".",
|
| 35 |
+
",",
|
| 36 |
+
),
|
| 37 |
+
],
|
| 38 |
+
)
|
| 39 |
+
def test_1000_sep_with_decimal(all_parsers, data, thousands, decimal):
|
| 40 |
+
parser = all_parsers
|
| 41 |
+
expected = DataFrame({"A": [1, 10], "B": [2334.01, 13], "C": [5, 10.0]})
|
| 42 |
+
|
| 43 |
+
if parser.engine == "pyarrow":
|
| 44 |
+
msg = "The 'thousands' option is not supported with the 'pyarrow' engine"
|
| 45 |
+
with pytest.raises(ValueError, match=msg):
|
| 46 |
+
parser.read_csv(
|
| 47 |
+
StringIO(data), sep="|", thousands=thousands, decimal=decimal
|
| 48 |
+
)
|
| 49 |
+
return
|
| 50 |
+
|
| 51 |
+
result = parser.read_csv(
|
| 52 |
+
StringIO(data), sep="|", thousands=thousands, decimal=decimal
|
| 53 |
+
)
|
| 54 |
+
tm.assert_frame_equal(result, expected)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def test_euro_decimal_format(all_parsers):
|
| 58 |
+
parser = all_parsers
|
| 59 |
+
data = """Id;Number1;Number2;Text1;Text2;Number3
|
| 60 |
+
1;1521,1541;187101,9543;ABC;poi;4,738797819
|
| 61 |
+
2;121,12;14897,76;DEF;uyt;0,377320872
|
| 62 |
+
3;878,158;108013,434;GHI;rez;2,735694704"""
|
| 63 |
+
|
| 64 |
+
result = parser.read_csv(StringIO(data), sep=";", decimal=",")
|
| 65 |
+
expected = DataFrame(
|
| 66 |
+
[
|
| 67 |
+
[1, 1521.1541, 187101.9543, "ABC", "poi", 4.738797819],
|
| 68 |
+
[2, 121.12, 14897.76, "DEF", "uyt", 0.377320872],
|
| 69 |
+
[3, 878.158, 108013.434, "GHI", "rez", 2.735694704],
|
| 70 |
+
],
|
| 71 |
+
columns=["Id", "Number1", "Number2", "Text1", "Text2", "Number3"],
|
| 72 |
+
)
|
| 73 |
+
tm.assert_frame_equal(result, expected)
|
.venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_file_buffer_url.py
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tests that work on both the Python and C engines but do not have a
|
| 3 |
+
specific classification into the other test modules.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from io import (
|
| 7 |
+
BytesIO,
|
| 8 |
+
StringIO,
|
| 9 |
+
)
|
| 10 |
+
import os
|
| 11 |
+
import platform
|
| 12 |
+
from urllib.error import URLError
|
| 13 |
+
import uuid
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
import pytest
|
| 17 |
+
|
| 18 |
+
from pandas.compat import WASM
|
| 19 |
+
from pandas.errors import (
|
| 20 |
+
EmptyDataError,
|
| 21 |
+
ParserError,
|
| 22 |
+
)
|
| 23 |
+
import pandas.util._test_decorators as td
|
| 24 |
+
|
| 25 |
+
from pandas import (
|
| 26 |
+
DataFrame,
|
| 27 |
+
Index,
|
| 28 |
+
)
|
| 29 |
+
import pandas._testing as tm
|
| 30 |
+
|
| 31 |
+
pytestmark = pytest.mark.filterwarnings(
|
| 32 |
+
"ignore:Passing a BlockManager to DataFrame:DeprecationWarning"
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
xfail_pyarrow = pytest.mark.usefixtures("pyarrow_xfail")
|
| 36 |
+
skip_pyarrow = pytest.mark.usefixtures("pyarrow_skip")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@pytest.mark.network
|
| 40 |
+
@pytest.mark.single_cpu
|
| 41 |
+
def test_url(all_parsers, csv_dir_path, httpserver):
|
| 42 |
+
parser = all_parsers
|
| 43 |
+
kwargs = {"sep": "\t"}
|
| 44 |
+
|
| 45 |
+
local_path = os.path.join(csv_dir_path, "salaries.csv")
|
| 46 |
+
with open(local_path, encoding="utf-8") as f:
|
| 47 |
+
httpserver.serve_content(content=f.read())
|
| 48 |
+
|
| 49 |
+
url_result = parser.read_csv(httpserver.url, **kwargs)
|
| 50 |
+
|
| 51 |
+
local_result = parser.read_csv(local_path, **kwargs)
|
| 52 |
+
tm.assert_frame_equal(url_result, local_result)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@pytest.mark.slow
|
| 56 |
+
def test_local_file(all_parsers, csv_dir_path):
|
| 57 |
+
parser = all_parsers
|
| 58 |
+
kwargs = {"sep": "\t"}
|
| 59 |
+
|
| 60 |
+
local_path = os.path.join(csv_dir_path, "salaries.csv")
|
| 61 |
+
local_result = parser.read_csv(local_path, **kwargs)
|
| 62 |
+
url = "file://localhost/" + local_path
|
| 63 |
+
|
| 64 |
+
try:
|
| 65 |
+
url_result = parser.read_csv(url, **kwargs)
|
| 66 |
+
tm.assert_frame_equal(url_result, local_result)
|
| 67 |
+
except URLError:
|
| 68 |
+
# Fails on some systems.
|
| 69 |
+
pytest.skip("Failing on: " + " ".join(platform.uname()))
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
@xfail_pyarrow # AssertionError: DataFrame.index are different
|
| 73 |
+
def test_path_path_lib(all_parsers, temp_file):
|
| 74 |
+
parser = all_parsers
|
| 75 |
+
df = DataFrame(
|
| 76 |
+
1.1 * np.arange(120).reshape((30, 4)),
|
| 77 |
+
columns=Index(list("ABCD")),
|
| 78 |
+
index=Index([f"i-{i}" for i in range(30)]),
|
| 79 |
+
)
|
| 80 |
+
result = tm.round_trip_pathlib(
|
| 81 |
+
df.to_csv, lambda p: parser.read_csv(p, index_col=0), temp_file
|
| 82 |
+
)
|
| 83 |
+
tm.assert_frame_equal(df, result)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
@pytest.mark.skipif(WASM, reason="limited file system access on WASM")
|
| 87 |
+
def test_nonexistent_path(all_parsers):
|
| 88 |
+
# gh-2428: pls no segfault
|
| 89 |
+
# gh-14086: raise more helpful FileNotFoundError
|
| 90 |
+
# GH#29233 "File foo" instead of "File b'foo'"
|
| 91 |
+
parser = all_parsers
|
| 92 |
+
path = f"{uuid.uuid4()}.csv"
|
| 93 |
+
|
| 94 |
+
msg = r"\[Errno 2\]"
|
| 95 |
+
with pytest.raises(FileNotFoundError, match=msg) as e:
|
| 96 |
+
parser.read_csv(path)
|
| 97 |
+
assert path == e.value.filename
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
@pytest.mark.skipif(WASM, reason="limited file system access on WASM")
|
| 101 |
+
@td.skip_if_windows # os.chmod does not work in windows
|
| 102 |
+
def test_no_permission(all_parsers, temp_file):
|
| 103 |
+
# GH 23784
|
| 104 |
+
parser = all_parsers
|
| 105 |
+
|
| 106 |
+
msg = r"\[Errno 13\]"
|
| 107 |
+
path = temp_file
|
| 108 |
+
os.chmod(path, 0) # make file unreadable
|
| 109 |
+
|
| 110 |
+
# verify that this process cannot open the file (not running as sudo)
|
| 111 |
+
try:
|
| 112 |
+
with open(path, encoding="utf-8"):
|
| 113 |
+
pass
|
| 114 |
+
pytest.skip("Running as sudo.")
|
| 115 |
+
except PermissionError:
|
| 116 |
+
pass
|
| 117 |
+
|
| 118 |
+
with pytest.raises(PermissionError, match=msg) as e:
|
| 119 |
+
parser.read_csv(path)
|
| 120 |
+
assert str(path.resolve()) == e.value.filename
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
@pytest.mark.parametrize(
|
| 124 |
+
"data,kwargs,expected,msg",
|
| 125 |
+
[
|
| 126 |
+
# gh-10728: WHITESPACE_LINE
|
| 127 |
+
(
|
| 128 |
+
"a,b,c\n4,5,6\n ",
|
| 129 |
+
{},
|
| 130 |
+
DataFrame([[4, 5, 6]], columns=["a", "b", "c"]),
|
| 131 |
+
None,
|
| 132 |
+
),
|
| 133 |
+
# gh-10548: EAT_LINE_COMMENT
|
| 134 |
+
(
|
| 135 |
+
"a,b,c\n4,5,6\n#comment",
|
| 136 |
+
{"comment": "#"},
|
| 137 |
+
DataFrame([[4, 5, 6]], columns=["a", "b", "c"]),
|
| 138 |
+
None,
|
| 139 |
+
),
|
| 140 |
+
# EAT_CRNL_NOP
|
| 141 |
+
(
|
| 142 |
+
"a,b,c\n4,5,6\n\r",
|
| 143 |
+
{},
|
| 144 |
+
DataFrame([[4, 5, 6]], columns=["a", "b", "c"]),
|
| 145 |
+
None,
|
| 146 |
+
),
|
| 147 |
+
# EAT_COMMENT
|
| 148 |
+
(
|
| 149 |
+
"a,b,c\n4,5,6#comment",
|
| 150 |
+
{"comment": "#"},
|
| 151 |
+
DataFrame([[4, 5, 6]], columns=["a", "b", "c"]),
|
| 152 |
+
None,
|
| 153 |
+
),
|
| 154 |
+
# SKIP_LINE
|
| 155 |
+
(
|
| 156 |
+
"a,b,c\n4,5,6\nskipme",
|
| 157 |
+
{"skiprows": [2]},
|
| 158 |
+
DataFrame([[4, 5, 6]], columns=["a", "b", "c"]),
|
| 159 |
+
None,
|
| 160 |
+
),
|
| 161 |
+
# EAT_LINE_COMMENT
|
| 162 |
+
(
|
| 163 |
+
"a,b,c\n4,5,6\n#comment",
|
| 164 |
+
{"comment": "#", "skip_blank_lines": False},
|
| 165 |
+
DataFrame([[4, 5, 6]], columns=["a", "b", "c"]),
|
| 166 |
+
None,
|
| 167 |
+
),
|
| 168 |
+
# IN_FIELD
|
| 169 |
+
(
|
| 170 |
+
"a,b,c\n4,5,6\n ",
|
| 171 |
+
{"skip_blank_lines": False},
|
| 172 |
+
DataFrame([["4", 5, 6], [" ", None, None]], columns=["a", "b", "c"]),
|
| 173 |
+
None,
|
| 174 |
+
),
|
| 175 |
+
# EAT_CRNL
|
| 176 |
+
(
|
| 177 |
+
"a,b,c\n4,5,6\n\r",
|
| 178 |
+
{"skip_blank_lines": False},
|
| 179 |
+
DataFrame([[4, 5, 6], [None, None, None]], columns=["a", "b", "c"]),
|
| 180 |
+
None,
|
| 181 |
+
),
|
| 182 |
+
# ESCAPED_CHAR
|
| 183 |
+
(
|
| 184 |
+
"a,b,c\n4,5,6\n\\",
|
| 185 |
+
{"escapechar": "\\"},
|
| 186 |
+
None,
|
| 187 |
+
"(EOF following escape character)|(unexpected end of data)",
|
| 188 |
+
),
|
| 189 |
+
# ESCAPE_IN_QUOTED_FIELD
|
| 190 |
+
(
|
| 191 |
+
'a,b,c\n4,5,6\n"\\',
|
| 192 |
+
{"escapechar": "\\"},
|
| 193 |
+
None,
|
| 194 |
+
"(EOF inside string starting at row 2)|(unexpected end of data)",
|
| 195 |
+
),
|
| 196 |
+
# IN_QUOTED_FIELD
|
| 197 |
+
(
|
| 198 |
+
'a,b,c\n4,5,6\n"',
|
| 199 |
+
{"escapechar": "\\"},
|
| 200 |
+
None,
|
| 201 |
+
"(EOF inside string starting at row 2)|(unexpected end of data)",
|
| 202 |
+
),
|
| 203 |
+
],
|
| 204 |
+
ids=[
|
| 205 |
+
"whitespace-line",
|
| 206 |
+
"eat-line-comment",
|
| 207 |
+
"eat-crnl-nop",
|
| 208 |
+
"eat-comment",
|
| 209 |
+
"skip-line",
|
| 210 |
+
"eat-line-comment",
|
| 211 |
+
"in-field",
|
| 212 |
+
"eat-crnl",
|
| 213 |
+
"escaped-char",
|
| 214 |
+
"escape-in-quoted-field",
|
| 215 |
+
"in-quoted-field",
|
| 216 |
+
],
|
| 217 |
+
)
|
| 218 |
+
def test_eof_states(all_parsers, data, kwargs, expected, msg, request):
|
| 219 |
+
# see gh-10728, gh-10548
|
| 220 |
+
parser = all_parsers
|
| 221 |
+
|
| 222 |
+
if parser.engine == "pyarrow" and "comment" in kwargs:
|
| 223 |
+
msg = "The 'comment' option is not supported with the 'pyarrow' engine"
|
| 224 |
+
with pytest.raises(ValueError, match=msg):
|
| 225 |
+
parser.read_csv(StringIO(data), **kwargs)
|
| 226 |
+
return
|
| 227 |
+
|
| 228 |
+
if parser.engine == "pyarrow" and "\r" not in data:
|
| 229 |
+
# pandas.errors.ParserError: CSV parse error: Expected 3 columns, got 1:
|
| 230 |
+
# ValueError: skiprows argument must be an integer when using engine='pyarrow'
|
| 231 |
+
# AssertionError: Regex pattern did not match.
|
| 232 |
+
pytest.skip(reason="https://github.com/apache/arrow/issues/38676")
|
| 233 |
+
|
| 234 |
+
if expected is None:
|
| 235 |
+
with pytest.raises(ParserError, match=msg):
|
| 236 |
+
parser.read_csv(StringIO(data), **kwargs)
|
| 237 |
+
else:
|
| 238 |
+
result = parser.read_csv(StringIO(data), **kwargs)
|
| 239 |
+
tm.assert_frame_equal(result, expected)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def test_temporary_file(all_parsers, temp_file):
|
| 243 |
+
# see gh-13398
|
| 244 |
+
parser = all_parsers
|
| 245 |
+
data = "0 0"
|
| 246 |
+
|
| 247 |
+
with temp_file.open(mode="w+", encoding="utf-8") as new_file:
|
| 248 |
+
new_file.write(data)
|
| 249 |
+
new_file.flush()
|
| 250 |
+
new_file.seek(0)
|
| 251 |
+
|
| 252 |
+
if parser.engine == "pyarrow":
|
| 253 |
+
msg = "the 'pyarrow' engine does not support regex separators"
|
| 254 |
+
with pytest.raises(ValueError, match=msg):
|
| 255 |
+
parser.read_csv(new_file, sep=r"\s+", header=None)
|
| 256 |
+
return
|
| 257 |
+
|
| 258 |
+
result = parser.read_csv(new_file, sep=r"\s+", header=None)
|
| 259 |
+
|
| 260 |
+
expected = DataFrame([[0, 0]])
|
| 261 |
+
tm.assert_frame_equal(result, expected)
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def test_internal_eof_byte(all_parsers):
|
| 265 |
+
# see gh-5500
|
| 266 |
+
parser = all_parsers
|
| 267 |
+
data = "a,b\n1\x1a,2"
|
| 268 |
+
|
| 269 |
+
expected = DataFrame([["1\x1a", 2]], columns=["a", "b"])
|
| 270 |
+
result = parser.read_csv(StringIO(data))
|
| 271 |
+
tm.assert_frame_equal(result, expected)
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
def test_internal_eof_byte_to_file(all_parsers, temp_file):
|
| 275 |
+
# see gh-16559
|
| 276 |
+
parser = all_parsers
|
| 277 |
+
data = b'c1,c2\r\n"test \x1a test", test\r\n'
|
| 278 |
+
expected = DataFrame([["test \x1a test", " test"]], columns=["c1", "c2"])
|
| 279 |
+
path = f"__{uuid.uuid4()}__.csv"
|
| 280 |
+
|
| 281 |
+
path2 = temp_file.parent / path
|
| 282 |
+
with open(path2, "wb") as f:
|
| 283 |
+
f.write(data)
|
| 284 |
+
|
| 285 |
+
result = parser.read_csv(path2)
|
| 286 |
+
tm.assert_frame_equal(result, expected)
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
def test_file_handle_string_io(all_parsers):
|
| 290 |
+
# gh-14418
|
| 291 |
+
#
|
| 292 |
+
# Don't close user provided file handles.
|
| 293 |
+
parser = all_parsers
|
| 294 |
+
data = "a,b\n1,2"
|
| 295 |
+
|
| 296 |
+
fh = StringIO(data)
|
| 297 |
+
parser.read_csv(fh)
|
| 298 |
+
assert not fh.closed
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
def test_file_handles_with_open(all_parsers, csv1):
|
| 302 |
+
# gh-14418
|
| 303 |
+
#
|
| 304 |
+
# Don't close user provided file handles.
|
| 305 |
+
parser = all_parsers
|
| 306 |
+
|
| 307 |
+
for mode in ["r", "rb"]:
|
| 308 |
+
with open(csv1, mode, encoding="utf-8" if mode == "r" else None) as f:
|
| 309 |
+
parser.read_csv(f)
|
| 310 |
+
assert not f.closed
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
def test_invalid_file_buffer_class(all_parsers):
|
| 314 |
+
# see gh-15337
|
| 315 |
+
class InvalidBuffer:
|
| 316 |
+
pass
|
| 317 |
+
|
| 318 |
+
parser = all_parsers
|
| 319 |
+
msg = "Invalid file path or buffer object type"
|
| 320 |
+
|
| 321 |
+
with pytest.raises(ValueError, match=msg):
|
| 322 |
+
parser.read_csv(InvalidBuffer())
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
def test_invalid_file_buffer_mock(all_parsers):
|
| 326 |
+
# see gh-15337
|
| 327 |
+
parser = all_parsers
|
| 328 |
+
msg = "Invalid file path or buffer object type"
|
| 329 |
+
|
| 330 |
+
class Foo:
|
| 331 |
+
pass
|
| 332 |
+
|
| 333 |
+
with pytest.raises(ValueError, match=msg):
|
| 334 |
+
parser.read_csv(Foo())
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
def test_valid_file_buffer_seems_invalid(all_parsers):
|
| 338 |
+
# gh-16135: we want to ensure that "tell" and "seek"
|
| 339 |
+
# aren't actually being used when we call `read_csv`
|
| 340 |
+
#
|
| 341 |
+
# Thus, while the object may look "invalid" (these
|
| 342 |
+
# methods are attributes of the `StringIO` class),
|
| 343 |
+
# it is still a valid file-object for our purposes.
|
| 344 |
+
class NoSeekTellBuffer(StringIO):
|
| 345 |
+
def tell(self):
|
| 346 |
+
raise AttributeError("No tell method")
|
| 347 |
+
|
| 348 |
+
def seek(self, pos, whence=0):
|
| 349 |
+
raise AttributeError("No seek method")
|
| 350 |
+
|
| 351 |
+
data = "a\n1"
|
| 352 |
+
parser = all_parsers
|
| 353 |
+
expected = DataFrame({"a": [1]})
|
| 354 |
+
|
| 355 |
+
result = parser.read_csv(NoSeekTellBuffer(data))
|
| 356 |
+
tm.assert_frame_equal(result, expected)
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
@pytest.mark.parametrize("io_class", [StringIO, BytesIO])
|
| 360 |
+
@pytest.mark.parametrize("encoding", [None, "utf-8"])
|
| 361 |
+
def test_read_csv_file_handle(all_parsers, io_class, encoding):
|
| 362 |
+
"""
|
| 363 |
+
Test whether read_csv does not close user-provided file handles.
|
| 364 |
+
|
| 365 |
+
GH 36980
|
| 366 |
+
"""
|
| 367 |
+
parser = all_parsers
|
| 368 |
+
expected = DataFrame({"a": [1], "b": [2]})
|
| 369 |
+
|
| 370 |
+
content = "a,b\n1,2"
|
| 371 |
+
handle = io_class(content.encode("utf-8") if io_class == BytesIO else content)
|
| 372 |
+
|
| 373 |
+
tm.assert_frame_equal(parser.read_csv(handle, encoding=encoding), expected)
|
| 374 |
+
assert not handle.closed
|
| 375 |
+
|
| 376 |
+
|
| 377 |
+
def test_memory_map_compression(all_parsers, compression, temp_file):
|
| 378 |
+
"""
|
| 379 |
+
Support memory map for compressed files.
|
| 380 |
+
|
| 381 |
+
GH 37621
|
| 382 |
+
"""
|
| 383 |
+
parser = all_parsers
|
| 384 |
+
expected = DataFrame({"a": [1], "b": [2]})
|
| 385 |
+
|
| 386 |
+
path = temp_file
|
| 387 |
+
expected.to_csv(path, index=False, compression=compression)
|
| 388 |
+
|
| 389 |
+
if parser.engine == "pyarrow":
|
| 390 |
+
msg = "The 'memory_map' option is not supported with the 'pyarrow' engine"
|
| 391 |
+
with pytest.raises(ValueError, match=msg):
|
| 392 |
+
parser.read_csv(path, memory_map=True, compression=compression)
|
| 393 |
+
return
|
| 394 |
+
|
| 395 |
+
result = parser.read_csv(path, memory_map=True, compression=compression)
|
| 396 |
+
|
| 397 |
+
tm.assert_frame_equal(
|
| 398 |
+
result,
|
| 399 |
+
expected,
|
| 400 |
+
)
|
| 401 |
+
|
| 402 |
+
|
| 403 |
+
def test_context_manager(all_parsers, datapath):
|
| 404 |
+
# make sure that opened files are closed
|
| 405 |
+
parser = all_parsers
|
| 406 |
+
|
| 407 |
+
path = datapath("io", "data", "csv", "iris.csv")
|
| 408 |
+
|
| 409 |
+
if parser.engine == "pyarrow":
|
| 410 |
+
msg = "The 'chunksize' option is not supported with the 'pyarrow' engine"
|
| 411 |
+
with pytest.raises(ValueError, match=msg):
|
| 412 |
+
parser.read_csv(path, chunksize=1)
|
| 413 |
+
return
|
| 414 |
+
|
| 415 |
+
reader = parser.read_csv(path, chunksize=1)
|
| 416 |
+
assert not reader.handles.handle.closed
|
| 417 |
+
try:
|
| 418 |
+
with reader:
|
| 419 |
+
next(reader)
|
| 420 |
+
raise AssertionError
|
| 421 |
+
except AssertionError:
|
| 422 |
+
assert reader.handles.handle.closed
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
def test_context_manageri_user_provided(all_parsers, datapath):
|
| 426 |
+
# make sure that user-provided handles are not closed
|
| 427 |
+
parser = all_parsers
|
| 428 |
+
|
| 429 |
+
with open(datapath("io", "data", "csv", "iris.csv"), encoding="utf-8") as path:
|
| 430 |
+
if parser.engine == "pyarrow":
|
| 431 |
+
msg = "The 'chunksize' option is not supported with the 'pyarrow' engine"
|
| 432 |
+
with pytest.raises(ValueError, match=msg):
|
| 433 |
+
parser.read_csv(path, chunksize=1)
|
| 434 |
+
return
|
| 435 |
+
|
| 436 |
+
reader = parser.read_csv(path, chunksize=1)
|
| 437 |
+
assert not reader.handles.handle.closed
|
| 438 |
+
try:
|
| 439 |
+
with reader:
|
| 440 |
+
next(reader)
|
| 441 |
+
raise AssertionError
|
| 442 |
+
except AssertionError:
|
| 443 |
+
assert not reader.handles.handle.closed
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
@skip_pyarrow # ParserError: Empty CSV file
|
| 447 |
+
def test_file_descriptor_leak(all_parsers, temp_file):
|
| 448 |
+
# GH 31488
|
| 449 |
+
parser = all_parsers
|
| 450 |
+
path = temp_file
|
| 451 |
+
with pytest.raises(EmptyDataError, match="No columns to parse from file"):
|
| 452 |
+
parser.read_csv(path)
|
| 453 |
+
|
| 454 |
+
|
| 455 |
+
def test_memory_map(all_parsers, csv_dir_path):
|
| 456 |
+
mmap_file = os.path.join(csv_dir_path, "test_mmap.csv")
|
| 457 |
+
parser = all_parsers
|
| 458 |
+
|
| 459 |
+
expected = DataFrame(
|
| 460 |
+
{"a": [1, 2, 3], "b": ["one", "two", "three"], "c": ["I", "II", "III"]}
|
| 461 |
+
)
|
| 462 |
+
|
| 463 |
+
if parser.engine == "pyarrow":
|
| 464 |
+
msg = "The 'memory_map' option is not supported with the 'pyarrow' engine"
|
| 465 |
+
with pytest.raises(ValueError, match=msg):
|
| 466 |
+
parser.read_csv(mmap_file, memory_map=True)
|
| 467 |
+
return
|
| 468 |
+
|
| 469 |
+
result = parser.read_csv(mmap_file, memory_map=True)
|
| 470 |
+
tm.assert_frame_equal(result, expected)
|
.venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_float.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tests that work on both the Python and C engines but do not have a
|
| 3 |
+
specific classification into the other test modules.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from io import StringIO
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import pytest
|
| 10 |
+
|
| 11 |
+
from pandas import DataFrame
|
| 12 |
+
import pandas._testing as tm
|
| 13 |
+
|
| 14 |
+
pytestmark = pytest.mark.filterwarnings(
|
| 15 |
+
"ignore:Passing a BlockManager to DataFrame:DeprecationWarning"
|
| 16 |
+
)
|
| 17 |
+
skip_pyarrow = pytest.mark.usefixtures("pyarrow_skip")
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@skip_pyarrow # ParserError: CSV parse error: Empty CSV file or block
|
| 21 |
+
def test_float_parser(all_parsers):
|
| 22 |
+
# see gh-9565
|
| 23 |
+
parser = all_parsers
|
| 24 |
+
data = "45e-1,4.5,45.,inf,-inf"
|
| 25 |
+
result = parser.read_csv(StringIO(data), header=None)
|
| 26 |
+
|
| 27 |
+
expected = DataFrame([[float(s) for s in data.split(",")]])
|
| 28 |
+
tm.assert_frame_equal(result, expected)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def test_scientific_no_exponent(all_parsers_all_precisions):
|
| 32 |
+
# see gh-12215
|
| 33 |
+
df = DataFrame.from_dict({"w": ["2e"], "x": ["3E"], "y": ["42e"], "z": ["632E"]})
|
| 34 |
+
data = df.to_csv(index=False)
|
| 35 |
+
parser, precision = all_parsers_all_precisions
|
| 36 |
+
|
| 37 |
+
df_roundtrip = parser.read_csv(StringIO(data), float_precision=precision)
|
| 38 |
+
tm.assert_frame_equal(df_roundtrip, df)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@pytest.mark.parametrize(
|
| 42 |
+
"value, expected_value",
|
| 43 |
+
[
|
| 44 |
+
("0E-617", 0.0),
|
| 45 |
+
("0E99999999", 0.0),
|
| 46 |
+
("-0E99999999", 0.0),
|
| 47 |
+
("-0E-99999999", 0.0),
|
| 48 |
+
("10E-617", 0.0),
|
| 49 |
+
("10E-100000", 0.0),
|
| 50 |
+
("-10E-100000", 0.0),
|
| 51 |
+
("10e-99999999999", 0.0),
|
| 52 |
+
("10e-999999999999", 0.0),
|
| 53 |
+
("10e-9999999999999", 0.0),
|
| 54 |
+
("10E999", np.inf),
|
| 55 |
+
("-10e99999999999", -np.inf),
|
| 56 |
+
("10e99999999999", np.inf),
|
| 57 |
+
("10e999999999999", np.inf),
|
| 58 |
+
("10e9999999999999", np.inf),
|
| 59 |
+
("50060e8007123400", np.inf),
|
| 60 |
+
("-50060e8007123400", -np.inf),
|
| 61 |
+
],
|
| 62 |
+
)
|
| 63 |
+
def test_large_exponent(all_parsers_all_precisions, value, expected_value):
|
| 64 |
+
# GH#38753; GH#38794; GH#62740
|
| 65 |
+
parser, precision = all_parsers_all_precisions
|
| 66 |
+
|
| 67 |
+
data = f"data\n{value}"
|
| 68 |
+
result = parser.read_csv(StringIO(data), float_precision=precision)
|
| 69 |
+
expected = DataFrame({"data": [expected_value]})
|
| 70 |
+
tm.assert_frame_equal(result, expected)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@pytest.mark.parametrize(
|
| 74 |
+
"value, expected_value",
|
| 75 |
+
[
|
| 76 |
+
("32.0", 32.0),
|
| 77 |
+
("32e0", 32.0),
|
| 78 |
+
("3.2e1", 32.0),
|
| 79 |
+
("3.2e80", 3.2e80),
|
| 80 |
+
("3.2e-80", 3.2e-80),
|
| 81 |
+
("18446744073709551616.0", float(1 << 64)), # loses precision
|
| 82 |
+
("18446744073709551616.5", float(1 << 64)), # loses precision
|
| 83 |
+
("36893488147419103232.3", float(1 << 65)), # loses precision
|
| 84 |
+
],
|
| 85 |
+
)
|
| 86 |
+
def test_small_int_followed_by_float(
|
| 87 |
+
all_parsers_all_precisions, value, expected_value, request
|
| 88 |
+
):
|
| 89 |
+
# GH#51295
|
| 90 |
+
parser, precision = all_parsers_all_precisions
|
| 91 |
+
data = f"""data
|
| 92 |
+
42
|
| 93 |
+
{value}"""
|
| 94 |
+
result = parser.read_csv(StringIO(data), float_precision=precision)
|
| 95 |
+
expected = DataFrame({"data": [42.0, expected_value]})
|
| 96 |
+
|
| 97 |
+
tm.assert_frame_equal(result, expected)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
@pytest.mark.parametrize(
|
| 101 |
+
"value", ["81e31d04049863b72", "d81e31d04049863b72", "81e3104049863b72"]
|
| 102 |
+
)
|
| 103 |
+
def test_invalid_float_number(all_parsers_all_precisions, value):
|
| 104 |
+
# GH#62617
|
| 105 |
+
parser, precision = all_parsers_all_precisions
|
| 106 |
+
data = f"h1,h2,h3\ndata1,{value},data3"
|
| 107 |
+
|
| 108 |
+
result = parser.read_csv(StringIO(data), float_precision=precision)
|
| 109 |
+
expected = DataFrame({"h1": ["data1"], "h2": [value], "h3": "data3"})
|
| 110 |
+
tm.assert_frame_equal(result, expected)
|
.venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_index.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tests that work on both the Python and C engines but do not have a
|
| 3 |
+
specific classification into the other test modules.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from io import StringIO
|
| 8 |
+
import os
|
| 9 |
+
|
| 10 |
+
import pytest
|
| 11 |
+
|
| 12 |
+
from pandas import (
|
| 13 |
+
DataFrame,
|
| 14 |
+
Index,
|
| 15 |
+
MultiIndex,
|
| 16 |
+
)
|
| 17 |
+
import pandas._testing as tm
|
| 18 |
+
|
| 19 |
+
pytestmark = pytest.mark.filterwarnings(
|
| 20 |
+
"ignore:Passing a BlockManager to DataFrame:DeprecationWarning"
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
xfail_pyarrow = pytest.mark.usefixtures("pyarrow_xfail")
|
| 24 |
+
skip_pyarrow = pytest.mark.usefixtures("pyarrow_skip")
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@pytest.mark.parametrize(
|
| 28 |
+
"data,kwargs,expected",
|
| 29 |
+
[
|
| 30 |
+
(
|
| 31 |
+
"""foo,2,3,4,5
|
| 32 |
+
bar,7,8,9,10
|
| 33 |
+
baz,12,13,14,15
|
| 34 |
+
qux,12,13,14,15
|
| 35 |
+
foo2,12,13,14,15
|
| 36 |
+
bar2,12,13,14,15
|
| 37 |
+
""",
|
| 38 |
+
{"index_col": 0, "names": ["index", "A", "B", "C", "D"]},
|
| 39 |
+
DataFrame(
|
| 40 |
+
[
|
| 41 |
+
[2, 3, 4, 5],
|
| 42 |
+
[7, 8, 9, 10],
|
| 43 |
+
[12, 13, 14, 15],
|
| 44 |
+
[12, 13, 14, 15],
|
| 45 |
+
[12, 13, 14, 15],
|
| 46 |
+
[12, 13, 14, 15],
|
| 47 |
+
],
|
| 48 |
+
index=Index(["foo", "bar", "baz", "qux", "foo2", "bar2"], name="index"),
|
| 49 |
+
columns=["A", "B", "C", "D"],
|
| 50 |
+
),
|
| 51 |
+
),
|
| 52 |
+
(
|
| 53 |
+
"""foo,one,2,3,4,5
|
| 54 |
+
foo,two,7,8,9,10
|
| 55 |
+
foo,three,12,13,14,15
|
| 56 |
+
bar,one,12,13,14,15
|
| 57 |
+
bar,two,12,13,14,15
|
| 58 |
+
""",
|
| 59 |
+
{"index_col": [0, 1], "names": ["index1", "index2", "A", "B", "C", "D"]},
|
| 60 |
+
DataFrame(
|
| 61 |
+
[
|
| 62 |
+
[2, 3, 4, 5],
|
| 63 |
+
[7, 8, 9, 10],
|
| 64 |
+
[12, 13, 14, 15],
|
| 65 |
+
[12, 13, 14, 15],
|
| 66 |
+
[12, 13, 14, 15],
|
| 67 |
+
],
|
| 68 |
+
index=MultiIndex.from_tuples(
|
| 69 |
+
[
|
| 70 |
+
("foo", "one"),
|
| 71 |
+
("foo", "two"),
|
| 72 |
+
("foo", "three"),
|
| 73 |
+
("bar", "one"),
|
| 74 |
+
("bar", "two"),
|
| 75 |
+
],
|
| 76 |
+
names=["index1", "index2"],
|
| 77 |
+
),
|
| 78 |
+
columns=["A", "B", "C", "D"],
|
| 79 |
+
),
|
| 80 |
+
),
|
| 81 |
+
],
|
| 82 |
+
)
|
| 83 |
+
def test_pass_names_with_index(all_parsers, data, kwargs, expected):
|
| 84 |
+
parser = all_parsers
|
| 85 |
+
result = parser.read_csv(StringIO(data), **kwargs)
|
| 86 |
+
tm.assert_frame_equal(result, expected)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
@pytest.mark.parametrize("index_col", [[0, 1], [1, 0]])
|
| 90 |
+
def test_multi_index_no_level_names(
|
| 91 |
+
request, all_parsers, index_col, using_infer_string
|
| 92 |
+
):
|
| 93 |
+
data = """index1,index2,A,B,C,D
|
| 94 |
+
foo,one,2,3,4,5
|
| 95 |
+
foo,two,7,8,9,10
|
| 96 |
+
foo,three,12,13,14,15
|
| 97 |
+
bar,one,12,13,14,15
|
| 98 |
+
bar,two,12,13,14,15
|
| 99 |
+
"""
|
| 100 |
+
headless_data = "\n".join(data.split("\n")[1:])
|
| 101 |
+
|
| 102 |
+
names = ["A", "B", "C", "D"]
|
| 103 |
+
parser = all_parsers
|
| 104 |
+
|
| 105 |
+
result = parser.read_csv(
|
| 106 |
+
StringIO(headless_data), index_col=index_col, header=None, names=names
|
| 107 |
+
)
|
| 108 |
+
expected = parser.read_csv(StringIO(data), index_col=index_col)
|
| 109 |
+
|
| 110 |
+
# No index names in headless data.
|
| 111 |
+
expected.index.names = [None] * 2
|
| 112 |
+
tm.assert_frame_equal(result, expected)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
@skip_pyarrow
|
| 116 |
+
def test_multi_index_no_level_names_implicit(all_parsers):
|
| 117 |
+
parser = all_parsers
|
| 118 |
+
data = """A,B,C,D
|
| 119 |
+
foo,one,2,3,4,5
|
| 120 |
+
foo,two,7,8,9,10
|
| 121 |
+
foo,three,12,13,14,15
|
| 122 |
+
bar,one,12,13,14,15
|
| 123 |
+
bar,two,12,13,14,15
|
| 124 |
+
"""
|
| 125 |
+
|
| 126 |
+
result = parser.read_csv(StringIO(data))
|
| 127 |
+
expected = DataFrame(
|
| 128 |
+
[
|
| 129 |
+
[2, 3, 4, 5],
|
| 130 |
+
[7, 8, 9, 10],
|
| 131 |
+
[12, 13, 14, 15],
|
| 132 |
+
[12, 13, 14, 15],
|
| 133 |
+
[12, 13, 14, 15],
|
| 134 |
+
],
|
| 135 |
+
columns=["A", "B", "C", "D"],
|
| 136 |
+
index=MultiIndex.from_tuples(
|
| 137 |
+
[
|
| 138 |
+
("foo", "one"),
|
| 139 |
+
("foo", "two"),
|
| 140 |
+
("foo", "three"),
|
| 141 |
+
("bar", "one"),
|
| 142 |
+
("bar", "two"),
|
| 143 |
+
]
|
| 144 |
+
),
|
| 145 |
+
)
|
| 146 |
+
tm.assert_frame_equal(result, expected)
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
@xfail_pyarrow # TypeError: an integer is required
|
| 150 |
+
@pytest.mark.parametrize(
|
| 151 |
+
"data,columns,header",
|
| 152 |
+
[
|
| 153 |
+
("a,b", ["a", "b"], [0]),
|
| 154 |
+
(
|
| 155 |
+
"a,b\nc,d",
|
| 156 |
+
MultiIndex.from_tuples([("a", "c"), ("b", "d")]),
|
| 157 |
+
[0, 1],
|
| 158 |
+
),
|
| 159 |
+
],
|
| 160 |
+
)
|
| 161 |
+
@pytest.mark.parametrize("round_trip", [True, False])
|
| 162 |
+
def test_multi_index_blank_df(all_parsers, data, columns, header, round_trip):
|
| 163 |
+
# see gh-14545
|
| 164 |
+
parser = all_parsers
|
| 165 |
+
expected = DataFrame(columns=columns)
|
| 166 |
+
data = expected.to_csv(index=False) if round_trip else data
|
| 167 |
+
|
| 168 |
+
result = parser.read_csv(StringIO(data), header=header)
|
| 169 |
+
tm.assert_frame_equal(result, expected)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
@xfail_pyarrow # AssertionError: DataFrame.columns are different
|
| 173 |
+
def test_no_unnamed_index(all_parsers):
|
| 174 |
+
parser = all_parsers
|
| 175 |
+
data = """ id c0 c1 c2
|
| 176 |
+
0 1 0 a b
|
| 177 |
+
1 2 0 c d
|
| 178 |
+
2 2 2 e f
|
| 179 |
+
"""
|
| 180 |
+
result = parser.read_csv(StringIO(data), sep=" ")
|
| 181 |
+
expected = DataFrame(
|
| 182 |
+
[[0, 1, 0, "a", "b"], [1, 2, 0, "c", "d"], [2, 2, 2, "e", "f"]],
|
| 183 |
+
columns=["Unnamed: 0", "id", "c0", "c1", "c2"],
|
| 184 |
+
)
|
| 185 |
+
tm.assert_frame_equal(result, expected)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def test_read_duplicate_index_explicit(all_parsers):
|
| 189 |
+
data = """index,A,B,C,D
|
| 190 |
+
foo,2,3,4,5
|
| 191 |
+
bar,7,8,9,10
|
| 192 |
+
baz,12,13,14,15
|
| 193 |
+
qux,12,13,14,15
|
| 194 |
+
foo,12,13,14,15
|
| 195 |
+
bar,12,13,14,15
|
| 196 |
+
"""
|
| 197 |
+
parser = all_parsers
|
| 198 |
+
result = parser.read_csv(StringIO(data), index_col=0)
|
| 199 |
+
|
| 200 |
+
expected = DataFrame(
|
| 201 |
+
[
|
| 202 |
+
[2, 3, 4, 5],
|
| 203 |
+
[7, 8, 9, 10],
|
| 204 |
+
[12, 13, 14, 15],
|
| 205 |
+
[12, 13, 14, 15],
|
| 206 |
+
[12, 13, 14, 15],
|
| 207 |
+
[12, 13, 14, 15],
|
| 208 |
+
],
|
| 209 |
+
columns=["A", "B", "C", "D"],
|
| 210 |
+
index=Index(["foo", "bar", "baz", "qux", "foo", "bar"], name="index"),
|
| 211 |
+
)
|
| 212 |
+
tm.assert_frame_equal(result, expected)
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
@skip_pyarrow
|
| 216 |
+
def test_read_duplicate_index_implicit(all_parsers):
|
| 217 |
+
data = """A,B,C,D
|
| 218 |
+
foo,2,3,4,5
|
| 219 |
+
bar,7,8,9,10
|
| 220 |
+
baz,12,13,14,15
|
| 221 |
+
qux,12,13,14,15
|
| 222 |
+
foo,12,13,14,15
|
| 223 |
+
bar,12,13,14,15
|
| 224 |
+
"""
|
| 225 |
+
parser = all_parsers
|
| 226 |
+
result = parser.read_csv(StringIO(data))
|
| 227 |
+
|
| 228 |
+
expected = DataFrame(
|
| 229 |
+
[
|
| 230 |
+
[2, 3, 4, 5],
|
| 231 |
+
[7, 8, 9, 10],
|
| 232 |
+
[12, 13, 14, 15],
|
| 233 |
+
[12, 13, 14, 15],
|
| 234 |
+
[12, 13, 14, 15],
|
| 235 |
+
[12, 13, 14, 15],
|
| 236 |
+
],
|
| 237 |
+
columns=["A", "B", "C", "D"],
|
| 238 |
+
index=Index(["foo", "bar", "baz", "qux", "foo", "bar"]),
|
| 239 |
+
)
|
| 240 |
+
tm.assert_frame_equal(result, expected)
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
@skip_pyarrow
|
| 244 |
+
def test_read_csv_no_index_name(all_parsers, csv_dir_path):
|
| 245 |
+
parser = all_parsers
|
| 246 |
+
csv2 = os.path.join(csv_dir_path, "test2.csv")
|
| 247 |
+
result = parser.read_csv(csv2, index_col=0, parse_dates=True)
|
| 248 |
+
|
| 249 |
+
expected = DataFrame(
|
| 250 |
+
[
|
| 251 |
+
[0.980269, 3.685731, -0.364216805298, -1.159738, "foo"],
|
| 252 |
+
[1.047916, -0.041232, -0.16181208307, 0.212549, "bar"],
|
| 253 |
+
[0.498581, 0.731168, -0.537677223318, 1.346270, "baz"],
|
| 254 |
+
[1.120202, 1.567621, 0.00364077397681, 0.675253, "qux"],
|
| 255 |
+
[-0.487094, 0.571455, -1.6116394093, 0.103469, "foo2"],
|
| 256 |
+
],
|
| 257 |
+
columns=["A", "B", "C", "D", "E"],
|
| 258 |
+
index=Index(
|
| 259 |
+
[
|
| 260 |
+
datetime(2000, 1, 3),
|
| 261 |
+
datetime(2000, 1, 4),
|
| 262 |
+
datetime(2000, 1, 5),
|
| 263 |
+
datetime(2000, 1, 6),
|
| 264 |
+
datetime(2000, 1, 7),
|
| 265 |
+
],
|
| 266 |
+
dtype="M8[us]",
|
| 267 |
+
),
|
| 268 |
+
)
|
| 269 |
+
tm.assert_frame_equal(result, expected)
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
@skip_pyarrow
|
| 273 |
+
def test_empty_with_index(all_parsers):
|
| 274 |
+
# see gh-10184
|
| 275 |
+
data = "x,y"
|
| 276 |
+
parser = all_parsers
|
| 277 |
+
result = parser.read_csv(StringIO(data), index_col=0)
|
| 278 |
+
|
| 279 |
+
expected = DataFrame(columns=["y"], index=Index([], name="x"))
|
| 280 |
+
tm.assert_frame_equal(result, expected)
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
# CSV parse error: Empty CSV file or block: cannot infer number of columns
|
| 284 |
+
@skip_pyarrow
|
| 285 |
+
def test_empty_with_multi_index(all_parsers):
|
| 286 |
+
# see gh-10467
|
| 287 |
+
data = "x,y,z"
|
| 288 |
+
parser = all_parsers
|
| 289 |
+
result = parser.read_csv(StringIO(data), index_col=["x", "y"])
|
| 290 |
+
|
| 291 |
+
expected = DataFrame(
|
| 292 |
+
columns=["z"], index=MultiIndex.from_arrays([[]] * 2, names=["x", "y"])
|
| 293 |
+
)
|
| 294 |
+
tm.assert_frame_equal(result, expected)
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
# CSV parse error: Empty CSV file or block: cannot infer number of columns
|
| 298 |
+
@skip_pyarrow
|
| 299 |
+
def test_empty_with_reversed_multi_index(all_parsers):
|
| 300 |
+
data = "x,y,z"
|
| 301 |
+
parser = all_parsers
|
| 302 |
+
result = parser.read_csv(StringIO(data), index_col=[1, 0])
|
| 303 |
+
|
| 304 |
+
expected = DataFrame(
|
| 305 |
+
columns=["z"], index=MultiIndex.from_arrays([[]] * 2, names=["y", "x"])
|
| 306 |
+
)
|
| 307 |
+
tm.assert_frame_equal(result, expected)
|
.venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_inf.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tests that work on both the Python and C engines but do not have a
|
| 3 |
+
specific classification into the other test modules.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from io import StringIO
|
| 7 |
+
|
| 8 |
+
import pytest
|
| 9 |
+
|
| 10 |
+
from pandas import DataFrame
|
| 11 |
+
import pandas._testing as tm
|
| 12 |
+
|
| 13 |
+
pytestmark = pytest.mark.filterwarnings(
|
| 14 |
+
"ignore:Passing a BlockManager to DataFrame:DeprecationWarning"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
xfail_pyarrow = pytest.mark.usefixtures("pyarrow_xfail")
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@xfail_pyarrow # AssertionError: DataFrame.index are different
|
| 21 |
+
@pytest.mark.parametrize("na_filter", [True, False])
|
| 22 |
+
def test_inf_parsing(all_parsers, na_filter):
|
| 23 |
+
parser = all_parsers
|
| 24 |
+
data = """\
|
| 25 |
+
,A
|
| 26 |
+
a,inf
|
| 27 |
+
b,-inf
|
| 28 |
+
c,+Inf
|
| 29 |
+
d,-Inf
|
| 30 |
+
e,INF
|
| 31 |
+
f,-INF
|
| 32 |
+
g,+INf
|
| 33 |
+
h,-INf
|
| 34 |
+
i,inF
|
| 35 |
+
j,-inF"""
|
| 36 |
+
expected = DataFrame(
|
| 37 |
+
{"A": [float("inf"), float("-inf")] * 5},
|
| 38 |
+
index=["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"],
|
| 39 |
+
)
|
| 40 |
+
result = parser.read_csv(StringIO(data), index_col=0, na_filter=na_filter)
|
| 41 |
+
tm.assert_frame_equal(result, expected)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@xfail_pyarrow # AssertionError: DataFrame.index are different
|
| 45 |
+
@pytest.mark.parametrize("na_filter", [True, False])
|
| 46 |
+
def test_infinity_parsing(all_parsers, na_filter):
|
| 47 |
+
parser = all_parsers
|
| 48 |
+
data = """\
|
| 49 |
+
,A
|
| 50 |
+
a,Infinity
|
| 51 |
+
b,-Infinity
|
| 52 |
+
c,+Infinity
|
| 53 |
+
"""
|
| 54 |
+
expected = DataFrame(
|
| 55 |
+
{"A": [float("infinity"), float("-infinity"), float("+infinity")]},
|
| 56 |
+
index=["a", "b", "c"],
|
| 57 |
+
)
|
| 58 |
+
result = parser.read_csv(StringIO(data), index_col=0, na_filter=na_filter)
|
| 59 |
+
tm.assert_frame_equal(result, expected)
|
.venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_ints.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tests that work on both the Python and C engines but do not have a
|
| 3 |
+
specific classification into the other test modules.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from io import StringIO
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import pytest
|
| 10 |
+
|
| 11 |
+
from pandas import (
|
| 12 |
+
DataFrame,
|
| 13 |
+
Series,
|
| 14 |
+
)
|
| 15 |
+
import pandas._testing as tm
|
| 16 |
+
|
| 17 |
+
pytestmark = pytest.mark.filterwarnings(
|
| 18 |
+
"ignore:Passing a BlockManager to DataFrame:DeprecationWarning"
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
xfail_pyarrow = pytest.mark.usefixtures("pyarrow_xfail")
|
| 22 |
+
skip_pyarrow = pytest.mark.usefixtures("pyarrow_skip")
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def test_int_conversion(all_parsers):
|
| 26 |
+
data = """A,B
|
| 27 |
+
1.0,1
|
| 28 |
+
2.0,2
|
| 29 |
+
3.0,3
|
| 30 |
+
"""
|
| 31 |
+
parser = all_parsers
|
| 32 |
+
result = parser.read_csv(StringIO(data))
|
| 33 |
+
|
| 34 |
+
expected = DataFrame([[1.0, 1], [2.0, 2], [3.0, 3]], columns=["A", "B"])
|
| 35 |
+
tm.assert_frame_equal(result, expected)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@pytest.mark.parametrize(
|
| 39 |
+
"data,kwargs,expected",
|
| 40 |
+
[
|
| 41 |
+
(
|
| 42 |
+
"A,B\nTrue,1\nFalse,2\nTrue,3",
|
| 43 |
+
{},
|
| 44 |
+
[[True, 1], [False, 2], [True, 3]],
|
| 45 |
+
),
|
| 46 |
+
(
|
| 47 |
+
"A,B\nYES,1\nno,2\nyes,3\nNo,3\nYes,3",
|
| 48 |
+
{"true_values": ["yes", "Yes", "YES"], "false_values": ["no", "NO", "No"]},
|
| 49 |
+
[[True, 1], [False, 2], [True, 3], [False, 3], [True, 3]],
|
| 50 |
+
),
|
| 51 |
+
(
|
| 52 |
+
"A,B\nTRUE,1\nFALSE,2\nTRUE,3",
|
| 53 |
+
{},
|
| 54 |
+
[[True, 1], [False, 2], [True, 3]],
|
| 55 |
+
),
|
| 56 |
+
(
|
| 57 |
+
"A,B\nfoo,bar\nbar,foo",
|
| 58 |
+
{"true_values": ["foo"], "false_values": ["bar"]},
|
| 59 |
+
[[True, False], [False, True]],
|
| 60 |
+
),
|
| 61 |
+
],
|
| 62 |
+
)
|
| 63 |
+
def test_parse_bool(all_parsers, data, kwargs, expected):
|
| 64 |
+
parser = all_parsers
|
| 65 |
+
result = parser.read_csv(StringIO(data), **kwargs)
|
| 66 |
+
expected = DataFrame(expected, columns=["A", "B"])
|
| 67 |
+
tm.assert_frame_equal(result, expected)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def test_parse_integers_above_fp_precision(all_parsers):
|
| 71 |
+
data = """Numbers
|
| 72 |
+
17007000002000191
|
| 73 |
+
17007000002000191
|
| 74 |
+
17007000002000191
|
| 75 |
+
17007000002000191
|
| 76 |
+
17007000002000192
|
| 77 |
+
17007000002000192
|
| 78 |
+
17007000002000192
|
| 79 |
+
17007000002000192
|
| 80 |
+
17007000002000192
|
| 81 |
+
17007000002000194"""
|
| 82 |
+
parser = all_parsers
|
| 83 |
+
result = parser.read_csv(StringIO(data))
|
| 84 |
+
expected = DataFrame(
|
| 85 |
+
{
|
| 86 |
+
"Numbers": [
|
| 87 |
+
17007000002000191,
|
| 88 |
+
17007000002000191,
|
| 89 |
+
17007000002000191,
|
| 90 |
+
17007000002000191,
|
| 91 |
+
17007000002000192,
|
| 92 |
+
17007000002000192,
|
| 93 |
+
17007000002000192,
|
| 94 |
+
17007000002000192,
|
| 95 |
+
17007000002000192,
|
| 96 |
+
17007000002000194,
|
| 97 |
+
]
|
| 98 |
+
}
|
| 99 |
+
)
|
| 100 |
+
tm.assert_frame_equal(result, expected)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
@pytest.mark.parametrize("sep", [" ", r"\s+"])
|
| 104 |
+
def test_integer_overflow_bug(all_parsers, sep):
|
| 105 |
+
# see gh-2601
|
| 106 |
+
data = "65248E10 11\n55555E55 22\n"
|
| 107 |
+
parser = all_parsers
|
| 108 |
+
if parser.engine == "pyarrow" and sep != " ":
|
| 109 |
+
msg = "the 'pyarrow' engine does not support regex separators"
|
| 110 |
+
with pytest.raises(ValueError, match=msg):
|
| 111 |
+
parser.read_csv(StringIO(data), header=None, sep=sep)
|
| 112 |
+
return
|
| 113 |
+
|
| 114 |
+
result = parser.read_csv(StringIO(data), header=None, sep=sep)
|
| 115 |
+
expected = DataFrame([[6.5248e14, 11], [5.5555e59, 22]])
|
| 116 |
+
tm.assert_frame_equal(result, expected)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def test_int64_min_issues(all_parsers):
|
| 120 |
+
# see gh-2599
|
| 121 |
+
parser = all_parsers
|
| 122 |
+
data = "A,B\n0,0\n0,"
|
| 123 |
+
result = parser.read_csv(StringIO(data))
|
| 124 |
+
|
| 125 |
+
expected = DataFrame({"A": [0, 0], "B": [0, np.nan]})
|
| 126 |
+
tm.assert_frame_equal(result, expected)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
@pytest.mark.parametrize("conv", [None, np.int64, np.uint64])
|
| 130 |
+
def test_int64_overflow(all_parsers, conv, request):
|
| 131 |
+
data = """ID
|
| 132 |
+
00013007854817840016671868
|
| 133 |
+
00013007854817840016749251
|
| 134 |
+
00013007854817840016754630
|
| 135 |
+
00013007854817840016781876
|
| 136 |
+
00013007854817840017028824
|
| 137 |
+
00013007854817840017963235
|
| 138 |
+
00013007854817840018860166"""
|
| 139 |
+
parser = all_parsers
|
| 140 |
+
|
| 141 |
+
if conv is None:
|
| 142 |
+
# 13007854817840016671868 > UINT64_MAX, so this
|
| 143 |
+
# will overflow and return object as the dtype.
|
| 144 |
+
if parser.engine == "pyarrow":
|
| 145 |
+
mark = pytest.mark.xfail(reason="parses to float64")
|
| 146 |
+
request.applymarker(mark)
|
| 147 |
+
|
| 148 |
+
result = parser.read_csv(StringIO(data))
|
| 149 |
+
expected = DataFrame(
|
| 150 |
+
[
|
| 151 |
+
13007854817840016671868,
|
| 152 |
+
13007854817840016749251,
|
| 153 |
+
13007854817840016754630,
|
| 154 |
+
13007854817840016781876,
|
| 155 |
+
13007854817840017028824,
|
| 156 |
+
13007854817840017963235,
|
| 157 |
+
13007854817840018860166,
|
| 158 |
+
],
|
| 159 |
+
columns=["ID"],
|
| 160 |
+
)
|
| 161 |
+
tm.assert_frame_equal(result, expected)
|
| 162 |
+
else:
|
| 163 |
+
# 13007854817840016671868 > UINT64_MAX, so attempts
|
| 164 |
+
# to cast to either int64 or uint64 will result in
|
| 165 |
+
# an OverflowError being raised.
|
| 166 |
+
msg = "|".join(
|
| 167 |
+
[
|
| 168 |
+
"Python int too large to convert to C long",
|
| 169 |
+
"long too big to convert",
|
| 170 |
+
"int too big to convert",
|
| 171 |
+
]
|
| 172 |
+
)
|
| 173 |
+
err = OverflowError
|
| 174 |
+
if parser.engine == "pyarrow":
|
| 175 |
+
err = ValueError
|
| 176 |
+
msg = "The 'converters' option is not supported with the 'pyarrow' engine"
|
| 177 |
+
|
| 178 |
+
with pytest.raises(err, match=msg):
|
| 179 |
+
parser.read_csv(StringIO(data), converters={"ID": conv})
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
@skip_pyarrow # CSV parse error: Empty CSV file or block
|
| 183 |
+
@pytest.mark.parametrize(
|
| 184 |
+
"val", [np.iinfo(np.uint64).max, np.iinfo(np.int64).max, np.iinfo(np.int64).min]
|
| 185 |
+
)
|
| 186 |
+
def test_int64_uint64_range(all_parsers, val):
|
| 187 |
+
# These numbers fall right inside the int64-uint64
|
| 188 |
+
# range, so they should be parsed as integer.
|
| 189 |
+
parser = all_parsers
|
| 190 |
+
result = parser.read_csv(StringIO(str(val)), header=None)
|
| 191 |
+
|
| 192 |
+
expected = DataFrame([val])
|
| 193 |
+
tm.assert_frame_equal(result, expected)
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
@skip_pyarrow # CSV parse error: Empty CSV file or block
|
| 197 |
+
@pytest.mark.parametrize(
|
| 198 |
+
"val", [np.iinfo(np.uint64).max + 1, np.iinfo(np.int64).min - 1]
|
| 199 |
+
)
|
| 200 |
+
def test_outside_int64_uint64_range(all_parsers, val, request):
|
| 201 |
+
# These numbers fall just outside the int64-uint64
|
| 202 |
+
# range, so they should be parsed as object.
|
| 203 |
+
parser = all_parsers
|
| 204 |
+
|
| 205 |
+
result = parser.read_csv(StringIO(str(val)), header=None)
|
| 206 |
+
|
| 207 |
+
expected = DataFrame([val])
|
| 208 |
+
tm.assert_frame_equal(result, expected)
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
@skip_pyarrow # CSV parse error: Empty CSV file or block
|
| 212 |
+
@pytest.mark.parametrize(
|
| 213 |
+
"val", [np.iinfo(np.uint64).max + 1, np.iinfo(np.int64).min - 1]
|
| 214 |
+
)
|
| 215 |
+
def test_outside_int64_uint64_range_follow_str(all_parsers, val):
|
| 216 |
+
parser = all_parsers
|
| 217 |
+
|
| 218 |
+
result = parser.read_csv(StringIO(f"{val}\nabc"), header=None)
|
| 219 |
+
|
| 220 |
+
expected = DataFrame([str(val), "abc"])
|
| 221 |
+
tm.assert_frame_equal(result, expected)
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
@xfail_pyarrow # gets float64 dtype instead of object
|
| 225 |
+
@pytest.mark.parametrize("exp_data", [[str(-1), str(2**63)], [str(2**63), str(-1)]])
|
| 226 |
+
def test_numeric_range_too_wide(all_parsers, exp_data):
|
| 227 |
+
# No numerical dtype can hold both negative and uint64
|
| 228 |
+
# values, so they should be cast as string.
|
| 229 |
+
parser = all_parsers
|
| 230 |
+
data = "\n".join(exp_data)
|
| 231 |
+
expected = DataFrame(exp_data)
|
| 232 |
+
|
| 233 |
+
result = parser.read_csv(StringIO(data), header=None)
|
| 234 |
+
tm.assert_frame_equal(result, expected)
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def test_integer_precision(all_parsers):
|
| 238 |
+
# Gh 7072
|
| 239 |
+
s = """1,1;0;0;0;1;1;3844;3844;3844;1;1;1;1;1;1;0;0;1;1;0;0,,,4321583677327450765
|
| 240 |
+
5,1;0;0;0;1;1;843;843;843;1;1;1;1;1;1;0;0;1;1;0;0,64.0,;,4321113141090630389"""
|
| 241 |
+
parser = all_parsers
|
| 242 |
+
result = parser.read_csv(StringIO(s), header=None)[4]
|
| 243 |
+
expected = Series([4321583677327450765, 4321113141090630389], name=4)
|
| 244 |
+
tm.assert_series_equal(result, expected)
|
.venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_iterator.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tests that work on both the Python and C engines but do not have a
|
| 3 |
+
specific classification into the other test modules.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from io import StringIO
|
| 7 |
+
|
| 8 |
+
import pytest
|
| 9 |
+
|
| 10 |
+
from pandas import (
|
| 11 |
+
DataFrame,
|
| 12 |
+
concat,
|
| 13 |
+
)
|
| 14 |
+
import pandas._testing as tm
|
| 15 |
+
|
| 16 |
+
pytestmark = pytest.mark.filterwarnings(
|
| 17 |
+
"ignore:Passing a BlockManager to DataFrame:DeprecationWarning"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def test_iterator(all_parsers):
|
| 22 |
+
# see gh-6607
|
| 23 |
+
data = """index,A,B,C,D
|
| 24 |
+
foo,2,3,4,5
|
| 25 |
+
bar,7,8,9,10
|
| 26 |
+
baz,12,13,14,15
|
| 27 |
+
qux,12,13,14,15
|
| 28 |
+
foo2,12,13,14,15
|
| 29 |
+
bar2,12,13,14,15
|
| 30 |
+
"""
|
| 31 |
+
parser = all_parsers
|
| 32 |
+
kwargs = {"index_col": 0}
|
| 33 |
+
|
| 34 |
+
expected = parser.read_csv(StringIO(data), **kwargs)
|
| 35 |
+
|
| 36 |
+
if parser.engine == "pyarrow":
|
| 37 |
+
msg = "The 'iterator' option is not supported with the 'pyarrow' engine"
|
| 38 |
+
with pytest.raises(ValueError, match=msg):
|
| 39 |
+
parser.read_csv(StringIO(data), iterator=True, **kwargs)
|
| 40 |
+
return
|
| 41 |
+
|
| 42 |
+
with parser.read_csv(StringIO(data), iterator=True, **kwargs) as reader:
|
| 43 |
+
first_chunk = reader.read(3)
|
| 44 |
+
tm.assert_frame_equal(first_chunk, expected[:3])
|
| 45 |
+
|
| 46 |
+
last_chunk = reader.read(5)
|
| 47 |
+
tm.assert_frame_equal(last_chunk, expected[3:])
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def test_iterator2(all_parsers):
|
| 51 |
+
parser = all_parsers
|
| 52 |
+
data = """A,B,C
|
| 53 |
+
foo,1,2,3
|
| 54 |
+
bar,4,5,6
|
| 55 |
+
baz,7,8,9
|
| 56 |
+
"""
|
| 57 |
+
|
| 58 |
+
if parser.engine == "pyarrow":
|
| 59 |
+
msg = "The 'iterator' option is not supported with the 'pyarrow' engine"
|
| 60 |
+
with pytest.raises(ValueError, match=msg):
|
| 61 |
+
parser.read_csv(StringIO(data), iterator=True)
|
| 62 |
+
return
|
| 63 |
+
|
| 64 |
+
with parser.read_csv(StringIO(data), iterator=True) as reader:
|
| 65 |
+
result = list(reader)
|
| 66 |
+
|
| 67 |
+
expected = DataFrame(
|
| 68 |
+
[[1, 2, 3], [4, 5, 6], [7, 8, 9]],
|
| 69 |
+
index=["foo", "bar", "baz"],
|
| 70 |
+
columns=["A", "B", "C"],
|
| 71 |
+
)
|
| 72 |
+
tm.assert_frame_equal(result[0], expected)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def test_iterator_stop_on_chunksize(all_parsers):
|
| 76 |
+
# gh-3967: stopping iteration when chunksize is specified
|
| 77 |
+
parser = all_parsers
|
| 78 |
+
data = """A,B,C
|
| 79 |
+
foo,1,2,3
|
| 80 |
+
bar,4,5,6
|
| 81 |
+
baz,7,8,9
|
| 82 |
+
"""
|
| 83 |
+
if parser.engine == "pyarrow":
|
| 84 |
+
msg = "The 'chunksize' option is not supported with the 'pyarrow' engine"
|
| 85 |
+
with pytest.raises(ValueError, match=msg):
|
| 86 |
+
parser.read_csv(StringIO(data), chunksize=1)
|
| 87 |
+
return
|
| 88 |
+
|
| 89 |
+
with parser.read_csv(StringIO(data), chunksize=1) as reader:
|
| 90 |
+
result = list(reader)
|
| 91 |
+
|
| 92 |
+
assert len(result) == 3
|
| 93 |
+
expected = DataFrame(
|
| 94 |
+
[[1, 2, 3], [4, 5, 6], [7, 8, 9]],
|
| 95 |
+
index=["foo", "bar", "baz"],
|
| 96 |
+
columns=["A", "B", "C"],
|
| 97 |
+
)
|
| 98 |
+
tm.assert_frame_equal(concat(result), expected)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def test_nrows_iterator_without_chunksize(all_parsers):
|
| 102 |
+
# GH 59079
|
| 103 |
+
parser = all_parsers
|
| 104 |
+
data = """A,B,C
|
| 105 |
+
foo,1,2,3
|
| 106 |
+
bar,4,5,6
|
| 107 |
+
baz,7,8,9
|
| 108 |
+
"""
|
| 109 |
+
if parser.engine == "pyarrow":
|
| 110 |
+
msg = "The 'iterator' option is not supported with the 'pyarrow' engine"
|
| 111 |
+
with pytest.raises(ValueError, match=msg):
|
| 112 |
+
parser.read_csv(StringIO(data), iterator=True, nrows=2)
|
| 113 |
+
return
|
| 114 |
+
|
| 115 |
+
with parser.read_csv(StringIO(data), iterator=True, nrows=2) as reader:
|
| 116 |
+
result = reader.get_chunk()
|
| 117 |
+
|
| 118 |
+
expected = DataFrame(
|
| 119 |
+
[[1, 2, 3], [4, 5, 6]],
|
| 120 |
+
index=["foo", "bar"],
|
| 121 |
+
columns=["A", "B", "C"],
|
| 122 |
+
)
|
| 123 |
+
tm.assert_frame_equal(result, expected)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
@pytest.mark.parametrize(
|
| 127 |
+
"kwargs", [{"iterator": True, "chunksize": 1}, {"iterator": True}, {"chunksize": 1}]
|
| 128 |
+
)
|
| 129 |
+
def test_iterator_skipfooter_errors(all_parsers, kwargs):
|
| 130 |
+
msg = "'skipfooter' not supported for iteration"
|
| 131 |
+
parser = all_parsers
|
| 132 |
+
data = "a\n1\n2"
|
| 133 |
+
|
| 134 |
+
if parser.engine == "pyarrow":
|
| 135 |
+
msg = (
|
| 136 |
+
"The '(chunksize|iterator)' option is not supported with the "
|
| 137 |
+
"'pyarrow' engine"
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
with pytest.raises(ValueError, match=msg):
|
| 141 |
+
with parser.read_csv(StringIO(data), skipfooter=1, **kwargs) as _:
|
| 142 |
+
pass
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def test_iteration_open_handle(temp_file, all_parsers):
|
| 146 |
+
parser = all_parsers
|
| 147 |
+
kwargs = {"header": None}
|
| 148 |
+
|
| 149 |
+
with open(temp_file, "w", encoding="utf-8") as f:
|
| 150 |
+
f.write("AAA\nBBB\nCCC\nDDD\nEEE\nFFF\nGGG")
|
| 151 |
+
|
| 152 |
+
with open(temp_file, encoding="utf-8") as f:
|
| 153 |
+
for line in f:
|
| 154 |
+
if "CCC" in line:
|
| 155 |
+
break
|
| 156 |
+
|
| 157 |
+
result = parser.read_csv(f, **kwargs)
|
| 158 |
+
expected = DataFrame({0: ["DDD", "EEE", "FFF", "GGG"]})
|
| 159 |
+
tm.assert_frame_equal(result, expected)
|
.venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_read_errors.py
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tests that work on the Python, C and PyArrow engines but do not have a
|
| 3 |
+
specific classification into the other test modules.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import codecs
|
| 7 |
+
import csv
|
| 8 |
+
from io import StringIO
|
| 9 |
+
import os
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
import pytest
|
| 14 |
+
|
| 15 |
+
from pandas.errors import (
|
| 16 |
+
EmptyDataError,
|
| 17 |
+
ParserError,
|
| 18 |
+
ParserWarning,
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
from pandas import DataFrame
|
| 22 |
+
import pandas._testing as tm
|
| 23 |
+
|
| 24 |
+
xfail_pyarrow = pytest.mark.usefixtures("pyarrow_xfail")
|
| 25 |
+
skip_pyarrow = pytest.mark.usefixtures("pyarrow_skip")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def test_empty_decimal_marker(all_parsers):
|
| 29 |
+
data = """A|B|C
|
| 30 |
+
1|2,334|5
|
| 31 |
+
10|13|10.
|
| 32 |
+
"""
|
| 33 |
+
# Parsers support only length-1 decimals
|
| 34 |
+
msg = "Only length-1 decimal markers supported"
|
| 35 |
+
parser = all_parsers
|
| 36 |
+
|
| 37 |
+
if parser.engine == "pyarrow":
|
| 38 |
+
msg = (
|
| 39 |
+
"only single character unicode strings can be "
|
| 40 |
+
"converted to Py_UCS4, got length 0"
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
with pytest.raises(ValueError, match=msg):
|
| 44 |
+
parser.read_csv(StringIO(data), decimal="")
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def test_bad_stream_exception(all_parsers, csv_dir_path):
|
| 48 |
+
# see gh-13652
|
| 49 |
+
#
|
| 50 |
+
# This test validates that both the Python engine and C engine will
|
| 51 |
+
# raise UnicodeDecodeError instead of C engine raising ParserError
|
| 52 |
+
# and swallowing the exception that caused read to fail.
|
| 53 |
+
path = os.path.join(csv_dir_path, "sauron.SHIFT_JIS.csv")
|
| 54 |
+
codec = codecs.lookup("utf-8")
|
| 55 |
+
utf8 = codecs.lookup("utf-8")
|
| 56 |
+
parser = all_parsers
|
| 57 |
+
msg = "'utf-8' codec can't decode byte"
|
| 58 |
+
|
| 59 |
+
# Stream must be binary UTF8.
|
| 60 |
+
with (
|
| 61 |
+
open(path, "rb") as handle,
|
| 62 |
+
codecs.StreamRecoder(
|
| 63 |
+
handle, utf8.encode, utf8.decode, codec.streamreader, codec.streamwriter
|
| 64 |
+
) as stream,
|
| 65 |
+
):
|
| 66 |
+
with pytest.raises(UnicodeDecodeError, match=msg):
|
| 67 |
+
parser.read_csv(stream)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def test_malformed(all_parsers):
|
| 71 |
+
# see gh-6607
|
| 72 |
+
parser = all_parsers
|
| 73 |
+
data = """ignore
|
| 74 |
+
A,B,C
|
| 75 |
+
1,2,3 # comment
|
| 76 |
+
1,2,3,4,5
|
| 77 |
+
2,3,4
|
| 78 |
+
"""
|
| 79 |
+
msg = "Expected 3 fields in line 4, saw 5"
|
| 80 |
+
err = ParserError
|
| 81 |
+
if parser.engine == "pyarrow":
|
| 82 |
+
msg = "The 'comment' option is not supported with the 'pyarrow' engine"
|
| 83 |
+
err = ValueError
|
| 84 |
+
with pytest.raises(err, match=msg):
|
| 85 |
+
parser.read_csv(StringIO(data), header=1, comment="#")
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
@pytest.mark.parametrize("nrows", [5, 3, None])
|
| 89 |
+
def test_malformed_chunks(all_parsers, nrows):
|
| 90 |
+
data = """ignore
|
| 91 |
+
A,B,C
|
| 92 |
+
skip
|
| 93 |
+
1,2,3
|
| 94 |
+
3,5,10 # comment
|
| 95 |
+
1,2,3,4,5
|
| 96 |
+
2,3,4
|
| 97 |
+
"""
|
| 98 |
+
parser = all_parsers
|
| 99 |
+
|
| 100 |
+
if parser.engine == "pyarrow":
|
| 101 |
+
msg = "The 'iterator' option is not supported with the 'pyarrow' engine"
|
| 102 |
+
with pytest.raises(ValueError, match=msg):
|
| 103 |
+
parser.read_csv(
|
| 104 |
+
StringIO(data),
|
| 105 |
+
header=1,
|
| 106 |
+
comment="#",
|
| 107 |
+
iterator=True,
|
| 108 |
+
chunksize=1,
|
| 109 |
+
skiprows=[2],
|
| 110 |
+
)
|
| 111 |
+
return
|
| 112 |
+
|
| 113 |
+
msg = "Expected 3 fields in line 6, saw 5"
|
| 114 |
+
with parser.read_csv(
|
| 115 |
+
StringIO(data), header=1, comment="#", iterator=True, chunksize=1, skiprows=[2]
|
| 116 |
+
) as reader:
|
| 117 |
+
with pytest.raises(ParserError, match=msg):
|
| 118 |
+
reader.read(nrows)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
@xfail_pyarrow # does not raise
|
| 122 |
+
def test_catch_too_many_names(all_parsers):
|
| 123 |
+
# see gh-5156
|
| 124 |
+
data = """\
|
| 125 |
+
1,2,3
|
| 126 |
+
4,,6
|
| 127 |
+
7,8,9
|
| 128 |
+
10,11,12\n"""
|
| 129 |
+
parser = all_parsers
|
| 130 |
+
msg = (
|
| 131 |
+
"Too many columns specified: expected 4 and found 3"
|
| 132 |
+
if parser.engine == "c"
|
| 133 |
+
else "Number of passed names did not match number of header fields in the file"
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
with pytest.raises(ValueError, match=msg):
|
| 137 |
+
parser.read_csv(StringIO(data), header=0, names=["a", "b", "c", "d"])
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
@skip_pyarrow # CSV parse error: Empty CSV file or block
|
| 141 |
+
@pytest.mark.parametrize("nrows", [0, 1, 2, 3, 4, 5])
|
| 142 |
+
def test_raise_on_no_columns(all_parsers, nrows):
|
| 143 |
+
parser = all_parsers
|
| 144 |
+
data = "\n" * nrows
|
| 145 |
+
|
| 146 |
+
msg = "No columns to parse from file"
|
| 147 |
+
with pytest.raises(EmptyDataError, match=msg):
|
| 148 |
+
parser.read_csv(StringIO(data))
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def test_unexpected_keyword_parameter_exception(all_parsers):
|
| 152 |
+
# GH-34976
|
| 153 |
+
parser = all_parsers
|
| 154 |
+
|
| 155 |
+
msg = "{}\\(\\) got an unexpected keyword argument 'foo'"
|
| 156 |
+
with pytest.raises(TypeError, match=msg.format("read_csv")):
|
| 157 |
+
parser.read_csv("foo.csv", foo=1)
|
| 158 |
+
with pytest.raises(TypeError, match=msg.format("read_table")):
|
| 159 |
+
parser.read_table("foo.tsv", foo=1)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def test_suppress_error_output(all_parsers):
|
| 163 |
+
# see gh-15925
|
| 164 |
+
parser = all_parsers
|
| 165 |
+
data = "a\n1\n1,2,3\n4\n5,6,7"
|
| 166 |
+
expected = DataFrame({"a": [1, 4]})
|
| 167 |
+
|
| 168 |
+
result = parser.read_csv(StringIO(data), on_bad_lines="skip")
|
| 169 |
+
tm.assert_frame_equal(result, expected)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def test_error_bad_lines(all_parsers):
|
| 173 |
+
# see gh-15925
|
| 174 |
+
parser = all_parsers
|
| 175 |
+
data = "a\n1\n1,2,3\n4\n5,6,7"
|
| 176 |
+
|
| 177 |
+
msg = "Expected 1 fields in line 3, saw 3"
|
| 178 |
+
|
| 179 |
+
if parser.engine == "pyarrow":
|
| 180 |
+
# "CSV parse error: Expected 1 columns, got 3: 1,2,3"
|
| 181 |
+
pytest.skip(reason="https://github.com/apache/arrow/issues/38676")
|
| 182 |
+
|
| 183 |
+
with pytest.raises(ParserError, match=msg):
|
| 184 |
+
parser.read_csv(StringIO(data), on_bad_lines="error")
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def test_warn_bad_lines(all_parsers):
|
| 188 |
+
# see gh-15925
|
| 189 |
+
parser = all_parsers
|
| 190 |
+
data = "a\n1\n1,2,3\n4\n5,6,7"
|
| 191 |
+
expected = DataFrame({"a": [1, 4]})
|
| 192 |
+
match_msg = "Skipping line"
|
| 193 |
+
|
| 194 |
+
expected_warning = ParserWarning
|
| 195 |
+
if parser.engine == "pyarrow":
|
| 196 |
+
match_msg = "Expected 1 columns, but found 3: 1,2,3"
|
| 197 |
+
|
| 198 |
+
with tm.assert_produces_warning(
|
| 199 |
+
expected_warning, match=match_msg, check_stacklevel=False
|
| 200 |
+
):
|
| 201 |
+
result = parser.read_csv(StringIO(data), on_bad_lines="warn")
|
| 202 |
+
tm.assert_frame_equal(result, expected)
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def test_read_csv_wrong_num_columns(all_parsers):
|
| 206 |
+
# Too few columns.
|
| 207 |
+
data = """A,B,C,D,E,F
|
| 208 |
+
1,2,3,4,5,6
|
| 209 |
+
6,7,8,9,10,11,12
|
| 210 |
+
11,12,13,14,15,16
|
| 211 |
+
"""
|
| 212 |
+
parser = all_parsers
|
| 213 |
+
msg = "Expected 6 fields in line 3, saw 7"
|
| 214 |
+
|
| 215 |
+
if parser.engine == "pyarrow":
|
| 216 |
+
# Expected 6 columns, got 7: 6,7,8,9,10,11,12
|
| 217 |
+
pytest.skip(reason="https://github.com/apache/arrow/issues/38676")
|
| 218 |
+
|
| 219 |
+
with pytest.raises(ParserError, match=msg):
|
| 220 |
+
parser.read_csv(StringIO(data))
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def test_null_byte_char(request, all_parsers):
|
| 224 |
+
# see gh-2741
|
| 225 |
+
data = "\x00,foo"
|
| 226 |
+
names = ["a", "b"]
|
| 227 |
+
parser = all_parsers
|
| 228 |
+
|
| 229 |
+
if parser.engine in ["c", "python"]:
|
| 230 |
+
if parser.engine == "python":
|
| 231 |
+
request.applymarker(
|
| 232 |
+
pytest.mark.xfail(reason="This is read as an empty character not null")
|
| 233 |
+
)
|
| 234 |
+
expected = DataFrame([[np.nan, "foo"]], columns=names)
|
| 235 |
+
out = parser.read_csv(StringIO(data), names=names)
|
| 236 |
+
tm.assert_frame_equal(out, expected)
|
| 237 |
+
else:
|
| 238 |
+
if parser.engine == "pyarrow":
|
| 239 |
+
# CSV parse error: Empty CSV file or block: "
|
| 240 |
+
# cannot infer number of columns"
|
| 241 |
+
pytest.skip(reason="https://github.com/apache/arrow/issues/38676")
|
| 242 |
+
else:
|
| 243 |
+
msg = "NULL byte detected"
|
| 244 |
+
with pytest.raises(ParserError, match=msg):
|
| 245 |
+
parser.read_csv(StringIO(data), names=names)
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
@pytest.mark.filterwarnings("always::ResourceWarning")
|
| 249 |
+
def test_open_file(all_parsers, temp_file):
|
| 250 |
+
# GH 39024
|
| 251 |
+
parser = all_parsers
|
| 252 |
+
|
| 253 |
+
msg = "Could not determine delimiter"
|
| 254 |
+
err = csv.Error
|
| 255 |
+
if parser.engine == "c":
|
| 256 |
+
msg = "object of type 'NoneType' has no len"
|
| 257 |
+
err = TypeError
|
| 258 |
+
elif parser.engine == "pyarrow":
|
| 259 |
+
msg = "'utf-8' codec can't decode byte 0xe4"
|
| 260 |
+
err = ValueError
|
| 261 |
+
|
| 262 |
+
file = Path(temp_file)
|
| 263 |
+
file.write_bytes(b"\xe4\na\n1")
|
| 264 |
+
|
| 265 |
+
with tm.assert_produces_warning(None):
|
| 266 |
+
# should not trigger a ResourceWarning
|
| 267 |
+
with pytest.raises(err, match=msg):
|
| 268 |
+
parser.read_csv(file, sep=None, encoding_errors="replace")
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def test_invalid_on_bad_line(all_parsers):
|
| 272 |
+
parser = all_parsers
|
| 273 |
+
data = "a\n1\n1,2,3\n4\n5,6,7"
|
| 274 |
+
with pytest.raises(ValueError, match="Argument abc is invalid for on_bad_lines"):
|
| 275 |
+
parser.read_csv(StringIO(data), on_bad_lines="abc")
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def test_bad_header_uniform_error(all_parsers):
|
| 279 |
+
parser = all_parsers
|
| 280 |
+
data = "+++123456789...\ncol1,col2,col3,col4\n1,2,3,4\n"
|
| 281 |
+
msg = "Expected 2 fields in line 2, saw 4"
|
| 282 |
+
if parser.engine == "c":
|
| 283 |
+
msg = (
|
| 284 |
+
"Could not construct index. Requested to use 1 "
|
| 285 |
+
"number of columns, but 3 left to parse."
|
| 286 |
+
)
|
| 287 |
+
elif parser.engine == "pyarrow":
|
| 288 |
+
# "CSV parse error: Expected 1 columns, got 4: col1,col2,col3,col4"
|
| 289 |
+
pytest.skip(reason="https://github.com/apache/arrow/issues/38676")
|
| 290 |
+
|
| 291 |
+
with pytest.raises(ParserError, match=msg):
|
| 292 |
+
parser.read_csv(StringIO(data), index_col=0, on_bad_lines="error")
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
def test_on_bad_lines_warn_correct_formatting(all_parsers):
|
| 296 |
+
# see gh-15925
|
| 297 |
+
parser = all_parsers
|
| 298 |
+
data = """1,2
|
| 299 |
+
a,b
|
| 300 |
+
a,b,c
|
| 301 |
+
a,b,d
|
| 302 |
+
a,b
|
| 303 |
+
"""
|
| 304 |
+
expected = DataFrame({"1": "a", "2": ["b"] * 2})
|
| 305 |
+
match_msg = "Skipping line"
|
| 306 |
+
|
| 307 |
+
expected_warning = ParserWarning
|
| 308 |
+
if parser.engine == "pyarrow":
|
| 309 |
+
match_msg = "Expected 2 columns, but found 3: a,b,c"
|
| 310 |
+
|
| 311 |
+
with tm.assert_produces_warning(
|
| 312 |
+
expected_warning, match=match_msg, check_stacklevel=False
|
| 313 |
+
):
|
| 314 |
+
result = parser.read_csv(StringIO(data), on_bad_lines="warn")
|
| 315 |
+
tm.assert_frame_equal(result, expected)
|
.venv/lib/python3.13/site-packages/pandas/tests/io/parser/conftest.py
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
import pytest
|
| 6 |
+
|
| 7 |
+
from pandas.compat import HAS_PYARROW
|
| 8 |
+
from pandas.compat._optional import VERSIONS
|
| 9 |
+
|
| 10 |
+
from pandas import (
|
| 11 |
+
read_csv,
|
| 12 |
+
read_table,
|
| 13 |
+
)
|
| 14 |
+
import pandas._testing as tm
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class BaseParser:
|
| 18 |
+
engine: str | None = None
|
| 19 |
+
low_memory = True
|
| 20 |
+
float_precision_choices: list[str | None] = []
|
| 21 |
+
|
| 22 |
+
def update_kwargs(self, kwargs):
|
| 23 |
+
kwargs = kwargs.copy()
|
| 24 |
+
kwargs.update({"engine": self.engine, "low_memory": self.low_memory})
|
| 25 |
+
|
| 26 |
+
return kwargs
|
| 27 |
+
|
| 28 |
+
def read_csv(self, *args, **kwargs):
|
| 29 |
+
kwargs = self.update_kwargs(kwargs)
|
| 30 |
+
return read_csv(*args, **kwargs)
|
| 31 |
+
|
| 32 |
+
def read_csv_check_warnings(
|
| 33 |
+
self,
|
| 34 |
+
warn_type: type[Warning],
|
| 35 |
+
warn_msg: str,
|
| 36 |
+
*args,
|
| 37 |
+
raise_on_extra_warnings=True,
|
| 38 |
+
check_stacklevel: bool = True,
|
| 39 |
+
**kwargs,
|
| 40 |
+
):
|
| 41 |
+
# We need to check the stacklevel here instead of in the tests
|
| 42 |
+
# since this is where read_csv is called and where the warning
|
| 43 |
+
# should point to.
|
| 44 |
+
kwargs = self.update_kwargs(kwargs)
|
| 45 |
+
with tm.assert_produces_warning(
|
| 46 |
+
warn_type,
|
| 47 |
+
match=warn_msg,
|
| 48 |
+
raise_on_extra_warnings=raise_on_extra_warnings,
|
| 49 |
+
check_stacklevel=check_stacklevel,
|
| 50 |
+
):
|
| 51 |
+
return read_csv(*args, **kwargs)
|
| 52 |
+
|
| 53 |
+
def read_table(self, *args, **kwargs):
|
| 54 |
+
kwargs = self.update_kwargs(kwargs)
|
| 55 |
+
return read_table(*args, **kwargs)
|
| 56 |
+
|
| 57 |
+
def read_table_check_warnings(
|
| 58 |
+
self,
|
| 59 |
+
warn_type: type[Warning],
|
| 60 |
+
warn_msg: str,
|
| 61 |
+
*args,
|
| 62 |
+
raise_on_extra_warnings=True,
|
| 63 |
+
**kwargs,
|
| 64 |
+
):
|
| 65 |
+
# We need to check the stacklevel here instead of in the tests
|
| 66 |
+
# since this is where read_table is called and where the warning
|
| 67 |
+
# should point to.
|
| 68 |
+
kwargs = self.update_kwargs(kwargs)
|
| 69 |
+
with tm.assert_produces_warning(
|
| 70 |
+
warn_type, match=warn_msg, raise_on_extra_warnings=raise_on_extra_warnings
|
| 71 |
+
):
|
| 72 |
+
return read_table(*args, **kwargs)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class CParser(BaseParser):
|
| 76 |
+
engine = "c"
|
| 77 |
+
float_precision_choices = [None, "high", "round_trip"]
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class CParserHighMemory(CParser):
|
| 81 |
+
low_memory = False
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
class CParserLowMemory(CParser):
|
| 85 |
+
low_memory = True
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
class PythonParser(BaseParser):
|
| 89 |
+
engine = "python"
|
| 90 |
+
float_precision_choices = [None]
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class PyArrowParser(BaseParser):
|
| 94 |
+
engine = "pyarrow"
|
| 95 |
+
float_precision_choices = [None]
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
@pytest.fixture
|
| 99 |
+
def csv_dir_path(datapath):
|
| 100 |
+
"""
|
| 101 |
+
The directory path to the data files needed for parser tests.
|
| 102 |
+
"""
|
| 103 |
+
return datapath("io", "parser", "data")
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
@pytest.fixture
|
| 107 |
+
def csv1(datapath):
|
| 108 |
+
"""
|
| 109 |
+
The path to the data file "test1.csv" needed for parser tests.
|
| 110 |
+
"""
|
| 111 |
+
return os.path.join(datapath("io", "data", "csv"), "test1.csv")
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
_cParserHighMemory = CParserHighMemory
|
| 115 |
+
_cParserLowMemory = CParserLowMemory
|
| 116 |
+
_pythonParser = PythonParser
|
| 117 |
+
_pyarrowParser = PyArrowParser
|
| 118 |
+
|
| 119 |
+
_py_parsers_only = [_pythonParser]
|
| 120 |
+
_c_parsers_only = [_cParserHighMemory, _cParserLowMemory]
|
| 121 |
+
_pyarrow_parsers_only = [
|
| 122 |
+
pytest.param(
|
| 123 |
+
_pyarrowParser,
|
| 124 |
+
marks=[
|
| 125 |
+
pytest.mark.single_cpu,
|
| 126 |
+
pytest.mark.skipif(not HAS_PYARROW, reason="pyarrow is not installed"),
|
| 127 |
+
],
|
| 128 |
+
)
|
| 129 |
+
]
|
| 130 |
+
|
| 131 |
+
_all_parsers = [*_c_parsers_only, *_py_parsers_only, *_pyarrow_parsers_only]
|
| 132 |
+
|
| 133 |
+
_py_parser_ids = ["python"]
|
| 134 |
+
_c_parser_ids = ["c_high", "c_low"]
|
| 135 |
+
_pyarrow_parsers_ids = ["pyarrow"]
|
| 136 |
+
|
| 137 |
+
_all_parser_ids = [*_c_parser_ids, *_py_parser_ids, *_pyarrow_parsers_ids]
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
@pytest.fixture(params=_all_parsers, ids=_all_parser_ids)
|
| 141 |
+
def all_parsers(request):
|
| 142 |
+
"""
|
| 143 |
+
Fixture all of the CSV parsers.
|
| 144 |
+
"""
|
| 145 |
+
parser = request.param()
|
| 146 |
+
if parser.engine == "pyarrow":
|
| 147 |
+
pytest.importorskip("pyarrow", VERSIONS["pyarrow"])
|
| 148 |
+
# Try finding a way to disable threads all together
|
| 149 |
+
# for more stable CI runs
|
| 150 |
+
import pyarrow
|
| 151 |
+
|
| 152 |
+
pyarrow.set_cpu_count(1)
|
| 153 |
+
return parser
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
@pytest.fixture(params=_c_parsers_only, ids=_c_parser_ids)
|
| 157 |
+
def c_parser_only(request):
|
| 158 |
+
"""
|
| 159 |
+
Fixture all of the CSV parsers using the C engine.
|
| 160 |
+
"""
|
| 161 |
+
return request.param()
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
@pytest.fixture(params=_py_parsers_only, ids=_py_parser_ids)
|
| 165 |
+
def python_parser_only(request):
|
| 166 |
+
"""
|
| 167 |
+
Fixture all of the CSV parsers using the Python engine.
|
| 168 |
+
"""
|
| 169 |
+
return request.param()
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
@pytest.fixture(params=_pyarrow_parsers_only, ids=_pyarrow_parsers_ids)
|
| 173 |
+
def pyarrow_parser_only(request):
|
| 174 |
+
"""
|
| 175 |
+
Fixture all of the CSV parsers using the Pyarrow engine.
|
| 176 |
+
"""
|
| 177 |
+
return request.param()
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def _get_all_parser_float_precision_combinations():
|
| 181 |
+
"""
|
| 182 |
+
Return all allowable parser and float precision
|
| 183 |
+
combinations and corresponding ids.
|
| 184 |
+
"""
|
| 185 |
+
params = []
|
| 186 |
+
ids = []
|
| 187 |
+
for parser, parser_id in zip(_all_parsers, _all_parser_ids):
|
| 188 |
+
if hasattr(parser, "values"):
|
| 189 |
+
# Wrapped in pytest.param, get the actual parser back
|
| 190 |
+
parser = parser.values[0]
|
| 191 |
+
for precision in parser.float_precision_choices:
|
| 192 |
+
# Re-wrap in pytest.param for pyarrow
|
| 193 |
+
mark = (
|
| 194 |
+
[
|
| 195 |
+
pytest.mark.single_cpu,
|
| 196 |
+
pytest.mark.skipif(
|
| 197 |
+
not HAS_PYARROW, reason="pyarrow is not installed"
|
| 198 |
+
),
|
| 199 |
+
]
|
| 200 |
+
if parser.engine == "pyarrow"
|
| 201 |
+
else ()
|
| 202 |
+
)
|
| 203 |
+
param = pytest.param((parser(), precision), marks=mark)
|
| 204 |
+
params.append(param)
|
| 205 |
+
ids.append(f"{parser_id}-{precision}")
|
| 206 |
+
|
| 207 |
+
return {"params": params, "ids": ids}
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
@pytest.fixture(
|
| 211 |
+
params=_get_all_parser_float_precision_combinations()["params"],
|
| 212 |
+
ids=_get_all_parser_float_precision_combinations()["ids"],
|
| 213 |
+
)
|
| 214 |
+
def all_parsers_all_precisions(request):
|
| 215 |
+
"""
|
| 216 |
+
Fixture for all allowable combinations of parser
|
| 217 |
+
and float precision
|
| 218 |
+
"""
|
| 219 |
+
return request.param
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
_utf_values = [8, 16, 32]
|
| 223 |
+
|
| 224 |
+
_encoding_seps = ["", "-", "_"]
|
| 225 |
+
_encoding_prefixes = ["utf", "UTF"]
|
| 226 |
+
|
| 227 |
+
_encoding_fmts = [
|
| 228 |
+
f"{prefix}{sep}{{0}}" for sep in _encoding_seps for prefix in _encoding_prefixes
|
| 229 |
+
]
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
@pytest.fixture(params=_utf_values)
|
| 233 |
+
def utf_value(request):
|
| 234 |
+
"""
|
| 235 |
+
Fixture for all possible integer values for a UTF encoding.
|
| 236 |
+
"""
|
| 237 |
+
return request.param
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
@pytest.fixture(params=_encoding_fmts)
|
| 241 |
+
def encoding_fmt(request):
|
| 242 |
+
"""
|
| 243 |
+
Fixture for all possible string formats of a UTF encoding.
|
| 244 |
+
"""
|
| 245 |
+
return request.param
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
@pytest.fixture(
|
| 249 |
+
params=[
|
| 250 |
+
("-1,0", -1.0),
|
| 251 |
+
("-1,2e0", -1.2),
|
| 252 |
+
("-1e0", -1.0),
|
| 253 |
+
("+1e0", 1.0),
|
| 254 |
+
("+1e+0", 1.0),
|
| 255 |
+
("+1e-1", 0.1),
|
| 256 |
+
("+,1e1", 1.0),
|
| 257 |
+
("+1,e0", 1.0),
|
| 258 |
+
("-,1e1", -1.0),
|
| 259 |
+
("-1,e0", -1.0),
|
| 260 |
+
("0,1", 0.1),
|
| 261 |
+
("1,", 1.0),
|
| 262 |
+
(",1", 0.1),
|
| 263 |
+
("-,1", -0.1),
|
| 264 |
+
("1_,", 1.0),
|
| 265 |
+
("1_234,56", 1234.56),
|
| 266 |
+
("1_234,56e0", 1234.56),
|
| 267 |
+
# negative cases; must not parse as float
|
| 268 |
+
("_", "_"),
|
| 269 |
+
("-_", "-_"),
|
| 270 |
+
("-_1", "-_1"),
|
| 271 |
+
("-_1e0", "-_1e0"),
|
| 272 |
+
("_1", "_1"),
|
| 273 |
+
("_1,", "_1,"),
|
| 274 |
+
("_1,_", "_1,_"),
|
| 275 |
+
("_1e0", "_1e0"),
|
| 276 |
+
("1,2e_1", "1,2e_1"),
|
| 277 |
+
("1,2e1_0", "1,2e1_0"),
|
| 278 |
+
("1,_2", "1,_2"),
|
| 279 |
+
(",1__2", ",1__2"),
|
| 280 |
+
(",1e", ",1e"),
|
| 281 |
+
("-,1e", "-,1e"),
|
| 282 |
+
("1_000,000_000", "1_000,000_000"),
|
| 283 |
+
("1,e1_2", "1,e1_2"),
|
| 284 |
+
("e11,2", "e11,2"),
|
| 285 |
+
("1e11,2", "1e11,2"),
|
| 286 |
+
("1,2,2", "1,2,2"),
|
| 287 |
+
("1,2_1", "1,2_1"),
|
| 288 |
+
("1,2e-10e1", "1,2e-10e1"),
|
| 289 |
+
("--1,2", "--1,2"),
|
| 290 |
+
("1a_2,1", "1a_2,1"),
|
| 291 |
+
("1,2E-1", 0.12),
|
| 292 |
+
("1,2E1", 12.0),
|
| 293 |
+
]
|
| 294 |
+
)
|
| 295 |
+
def numeric_decimal(request):
|
| 296 |
+
"""
|
| 297 |
+
Fixture for all numeric formats which should get recognized. The first entry
|
| 298 |
+
represents the value to read while the second represents the expected result.
|
| 299 |
+
"""
|
| 300 |
+
return request.param
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
@pytest.fixture
|
| 304 |
+
def pyarrow_xfail(request):
|
| 305 |
+
"""
|
| 306 |
+
Fixture that xfails a test if the engine is pyarrow.
|
| 307 |
+
|
| 308 |
+
Use if failure is do to unsupported keywords or inconsistent results.
|
| 309 |
+
"""
|
| 310 |
+
if "all_parsers" in request.fixturenames:
|
| 311 |
+
parser = request.getfixturevalue("all_parsers")
|
| 312 |
+
elif "all_parsers_all_precisions" in request.fixturenames:
|
| 313 |
+
# Return value is tuple of (engine, precision)
|
| 314 |
+
parser = request.getfixturevalue("all_parsers_all_precisions")[0]
|
| 315 |
+
else:
|
| 316 |
+
return
|
| 317 |
+
if parser.engine == "pyarrow":
|
| 318 |
+
mark = pytest.mark.xfail(reason="pyarrow doesn't support this.")
|
| 319 |
+
request.applymarker(mark)
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
@pytest.fixture
|
| 323 |
+
def pyarrow_skip(request):
|
| 324 |
+
"""
|
| 325 |
+
Fixture that skips a test if the engine is pyarrow.
|
| 326 |
+
|
| 327 |
+
Use if failure is do a parsing failure from pyarrow.csv.read_csv
|
| 328 |
+
"""
|
| 329 |
+
if "all_parsers" in request.fixturenames:
|
| 330 |
+
parser = request.getfixturevalue("all_parsers")
|
| 331 |
+
elif "all_parsers_all_precisions" in request.fixturenames:
|
| 332 |
+
# Return value is tuple of (engine, precision)
|
| 333 |
+
parser = request.getfixturevalue("all_parsers_all_precisions")[0]
|
| 334 |
+
else:
|
| 335 |
+
return
|
| 336 |
+
if parser.engine == "pyarrow":
|
| 337 |
+
pytest.skip(reason="https://github.com/apache/arrow/issues/38676")
|
.venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/__init__.py
ADDED
|
File without changes
|
.venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/test_categorical.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tests dtype specification during parsing
|
| 3 |
+
for all of the parsers defined in parsers.py
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from io import StringIO
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
import numpy as np
|
| 10 |
+
import pytest
|
| 11 |
+
|
| 12 |
+
from pandas._libs import parsers as libparsers
|
| 13 |
+
from pandas.errors import Pandas4Warning
|
| 14 |
+
|
| 15 |
+
from pandas.core.dtypes.dtypes import CategoricalDtype
|
| 16 |
+
|
| 17 |
+
import pandas as pd
|
| 18 |
+
from pandas import (
|
| 19 |
+
Categorical,
|
| 20 |
+
DataFrame,
|
| 21 |
+
Timestamp,
|
| 22 |
+
)
|
| 23 |
+
import pandas._testing as tm
|
| 24 |
+
|
| 25 |
+
pytestmark = pytest.mark.filterwarnings(
|
| 26 |
+
"ignore:Passing a BlockManager to DataFrame:DeprecationWarning"
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
xfail_pyarrow = pytest.mark.usefixtures("pyarrow_xfail")
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@xfail_pyarrow # AssertionError: Attributes of DataFrame.iloc[:, 0] are different
|
| 33 |
+
@pytest.mark.parametrize(
|
| 34 |
+
"dtype",
|
| 35 |
+
[
|
| 36 |
+
"category",
|
| 37 |
+
CategoricalDtype(),
|
| 38 |
+
{"a": "category", "b": "category", "c": CategoricalDtype()},
|
| 39 |
+
],
|
| 40 |
+
)
|
| 41 |
+
def test_categorical_dtype(all_parsers, dtype):
|
| 42 |
+
# see gh-10153
|
| 43 |
+
parser = all_parsers
|
| 44 |
+
data = """a,b,c
|
| 45 |
+
1,a,3.4
|
| 46 |
+
1,a,3.4
|
| 47 |
+
2,b,4.5"""
|
| 48 |
+
expected = DataFrame(
|
| 49 |
+
{
|
| 50 |
+
"a": Categorical(["1", "1", "2"]),
|
| 51 |
+
"b": Categorical(["a", "a", "b"]),
|
| 52 |
+
"c": Categorical(["3.4", "3.4", "4.5"]),
|
| 53 |
+
}
|
| 54 |
+
)
|
| 55 |
+
actual = parser.read_csv(StringIO(data), dtype=dtype)
|
| 56 |
+
tm.assert_frame_equal(actual, expected)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@pytest.mark.parametrize("dtype", [{"b": "category"}, {1: "category"}])
|
| 60 |
+
def test_categorical_dtype_single(all_parsers, dtype, request):
|
| 61 |
+
# see gh-10153
|
| 62 |
+
parser = all_parsers
|
| 63 |
+
data = """a,b,c
|
| 64 |
+
1,a,3.4
|
| 65 |
+
1,a,3.4
|
| 66 |
+
2,b,4.5"""
|
| 67 |
+
expected = DataFrame(
|
| 68 |
+
{"a": [1, 1, 2], "b": Categorical(["a", "a", "b"]), "c": [3.4, 3.4, 4.5]}
|
| 69 |
+
)
|
| 70 |
+
if parser.engine == "pyarrow":
|
| 71 |
+
mark = pytest.mark.xfail(
|
| 72 |
+
strict=False,
|
| 73 |
+
reason="Flaky test sometimes gives object dtype instead of Categorical",
|
| 74 |
+
)
|
| 75 |
+
request.applymarker(mark)
|
| 76 |
+
|
| 77 |
+
actual = parser.read_csv(StringIO(data), dtype=dtype)
|
| 78 |
+
tm.assert_frame_equal(actual, expected)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
@xfail_pyarrow # AssertionError: Attributes of DataFrame.iloc[:, 0] are different
|
| 82 |
+
def test_categorical_dtype_unsorted(all_parsers):
|
| 83 |
+
# see gh-10153
|
| 84 |
+
parser = all_parsers
|
| 85 |
+
data = """a,b,c
|
| 86 |
+
1,b,3.4
|
| 87 |
+
1,b,3.4
|
| 88 |
+
2,a,4.5"""
|
| 89 |
+
expected = DataFrame(
|
| 90 |
+
{
|
| 91 |
+
"a": Categorical(["1", "1", "2"]),
|
| 92 |
+
"b": Categorical(["b", "b", "a"]),
|
| 93 |
+
"c": Categorical(["3.4", "3.4", "4.5"]),
|
| 94 |
+
}
|
| 95 |
+
)
|
| 96 |
+
actual = parser.read_csv(StringIO(data), dtype="category")
|
| 97 |
+
tm.assert_frame_equal(actual, expected)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
@xfail_pyarrow # AssertionError: Attributes of DataFrame.iloc[:, 0] are different
|
| 101 |
+
def test_categorical_dtype_missing(all_parsers):
|
| 102 |
+
# see gh-10153
|
| 103 |
+
parser = all_parsers
|
| 104 |
+
data = """a,b,c
|
| 105 |
+
1,b,3.4
|
| 106 |
+
1,nan,3.4
|
| 107 |
+
2,a,4.5"""
|
| 108 |
+
expected = DataFrame(
|
| 109 |
+
{
|
| 110 |
+
"a": Categorical(["1", "1", "2"]),
|
| 111 |
+
"b": Categorical(["b", np.nan, "a"]),
|
| 112 |
+
"c": Categorical(["3.4", "3.4", "4.5"]),
|
| 113 |
+
}
|
| 114 |
+
)
|
| 115 |
+
actual = parser.read_csv(StringIO(data), dtype="category")
|
| 116 |
+
tm.assert_frame_equal(actual, expected)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
@xfail_pyarrow # AssertionError: Attributes of DataFrame.iloc[:, 0] are different
|
| 120 |
+
@pytest.mark.slow
|
| 121 |
+
def test_categorical_dtype_high_cardinality_numeric(all_parsers, monkeypatch):
|
| 122 |
+
# see gh-18186
|
| 123 |
+
# was an issue with C parser, due to DEFAULT_BUFFER_HEURISTIC
|
| 124 |
+
parser = all_parsers
|
| 125 |
+
heuristic = 2**5
|
| 126 |
+
data = np.sort([str(i) for i in range(heuristic + 1)])
|
| 127 |
+
expected = DataFrame({"a": Categorical(data, ordered=True)})
|
| 128 |
+
with monkeypatch.context() as m:
|
| 129 |
+
m.setattr(libparsers, "DEFAULT_BUFFER_HEURISTIC", heuristic)
|
| 130 |
+
actual = parser.read_csv(StringIO("a\n" + "\n".join(data)), dtype="category")
|
| 131 |
+
actual["a"] = actual["a"].cat.reorder_categories(
|
| 132 |
+
np.sort(actual.a.cat.categories), ordered=True
|
| 133 |
+
)
|
| 134 |
+
tm.assert_frame_equal(actual, expected)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def test_categorical_dtype_utf16(all_parsers, csv_dir_path):
|
| 138 |
+
# see gh-10153
|
| 139 |
+
pth = os.path.join(csv_dir_path, "utf16_ex.txt")
|
| 140 |
+
parser = all_parsers
|
| 141 |
+
encoding = "utf-16"
|
| 142 |
+
sep = "\t"
|
| 143 |
+
|
| 144 |
+
expected = parser.read_csv(pth, sep=sep, encoding=encoding)
|
| 145 |
+
expected = expected.apply(Categorical)
|
| 146 |
+
|
| 147 |
+
actual = parser.read_csv(pth, sep=sep, encoding=encoding, dtype="category")
|
| 148 |
+
tm.assert_frame_equal(actual, expected)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def test_categorical_dtype_chunksize_infer_categories(all_parsers):
|
| 152 |
+
# see gh-10153
|
| 153 |
+
parser = all_parsers
|
| 154 |
+
data = """a,b
|
| 155 |
+
1,a
|
| 156 |
+
1,b
|
| 157 |
+
1,b
|
| 158 |
+
2,c"""
|
| 159 |
+
expecteds = [
|
| 160 |
+
DataFrame({"a": [1, 1], "b": Categorical(["a", "b"])}),
|
| 161 |
+
DataFrame({"a": [1, 2], "b": Categorical(["b", "c"])}, index=[2, 3]),
|
| 162 |
+
]
|
| 163 |
+
|
| 164 |
+
if parser.engine == "pyarrow":
|
| 165 |
+
msg = "The 'chunksize' option is not supported with the 'pyarrow' engine"
|
| 166 |
+
with pytest.raises(ValueError, match=msg):
|
| 167 |
+
parser.read_csv(StringIO(data), dtype={"b": "category"}, chunksize=2)
|
| 168 |
+
return
|
| 169 |
+
|
| 170 |
+
with parser.read_csv(
|
| 171 |
+
StringIO(data), dtype={"b": "category"}, chunksize=2
|
| 172 |
+
) as actuals:
|
| 173 |
+
for actual, expected in zip(actuals, expecteds):
|
| 174 |
+
tm.assert_frame_equal(actual, expected)
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def test_categorical_dtype_chunksize_explicit_categories(all_parsers):
|
| 178 |
+
# see gh-10153
|
| 179 |
+
parser = all_parsers
|
| 180 |
+
data = """a,b
|
| 181 |
+
1,a
|
| 182 |
+
1,b
|
| 183 |
+
1,b
|
| 184 |
+
2,c"""
|
| 185 |
+
cats = ["a", "b", "c"]
|
| 186 |
+
expecteds = [
|
| 187 |
+
DataFrame({"a": [1, 1], "b": Categorical(["a", "b"], categories=cats)}),
|
| 188 |
+
DataFrame(
|
| 189 |
+
{"a": [1, 2], "b": Categorical(["b", "c"], categories=cats)},
|
| 190 |
+
index=[2, 3],
|
| 191 |
+
),
|
| 192 |
+
]
|
| 193 |
+
dtype = CategoricalDtype(cats)
|
| 194 |
+
|
| 195 |
+
if parser.engine == "pyarrow":
|
| 196 |
+
msg = "The 'chunksize' option is not supported with the 'pyarrow' engine"
|
| 197 |
+
with pytest.raises(ValueError, match=msg):
|
| 198 |
+
parser.read_csv(StringIO(data), dtype={"b": dtype}, chunksize=2)
|
| 199 |
+
return
|
| 200 |
+
|
| 201 |
+
with parser.read_csv(StringIO(data), dtype={"b": dtype}, chunksize=2) as actuals:
|
| 202 |
+
for actual, expected in zip(actuals, expecteds):
|
| 203 |
+
tm.assert_frame_equal(actual, expected)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def test_categorical_dtype_latin1(all_parsers, csv_dir_path):
|
| 207 |
+
# see gh-10153
|
| 208 |
+
pth = os.path.join(csv_dir_path, "unicode_series.csv")
|
| 209 |
+
parser = all_parsers
|
| 210 |
+
encoding = "latin-1"
|
| 211 |
+
|
| 212 |
+
expected = parser.read_csv(pth, header=None, encoding=encoding)
|
| 213 |
+
expected[1] = Categorical(expected[1])
|
| 214 |
+
|
| 215 |
+
actual = parser.read_csv(pth, header=None, encoding=encoding, dtype={1: "category"})
|
| 216 |
+
tm.assert_frame_equal(actual, expected)
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
@pytest.mark.parametrize("ordered", [False, True])
|
| 220 |
+
@pytest.mark.parametrize(
|
| 221 |
+
"categories",
|
| 222 |
+
[["a", "b", "c"], ["a", "c", "b"], ["a", "b", "c", "d"], ["c", "b", "a"]],
|
| 223 |
+
)
|
| 224 |
+
def test_categorical_category_dtype(all_parsers, categories, ordered):
|
| 225 |
+
parser = all_parsers
|
| 226 |
+
data = """a,b
|
| 227 |
+
1,a
|
| 228 |
+
1,b
|
| 229 |
+
1,b
|
| 230 |
+
2,c"""
|
| 231 |
+
expected = DataFrame(
|
| 232 |
+
{
|
| 233 |
+
"a": [1, 1, 1, 2],
|
| 234 |
+
"b": Categorical(
|
| 235 |
+
["a", "b", "b", "c"], categories=categories, ordered=ordered
|
| 236 |
+
),
|
| 237 |
+
}
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
dtype = {"b": CategoricalDtype(categories=categories, ordered=ordered)}
|
| 241 |
+
result = parser.read_csv(StringIO(data), dtype=dtype)
|
| 242 |
+
tm.assert_frame_equal(result, expected)
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def test_categorical_category_dtype_unsorted(all_parsers):
|
| 246 |
+
parser = all_parsers
|
| 247 |
+
data = """a,b
|
| 248 |
+
1,a
|
| 249 |
+
1,b
|
| 250 |
+
1,b
|
| 251 |
+
2,c"""
|
| 252 |
+
dtype = CategoricalDtype(["c", "b", "a"])
|
| 253 |
+
expected = DataFrame(
|
| 254 |
+
{
|
| 255 |
+
"a": [1, 1, 1, 2],
|
| 256 |
+
"b": Categorical(["a", "b", "b", "c"], categories=["c", "b", "a"]),
|
| 257 |
+
}
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
+
result = parser.read_csv(StringIO(data), dtype={"b": dtype})
|
| 261 |
+
tm.assert_frame_equal(result, expected)
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def test_categorical_coerces_numeric(all_parsers):
|
| 265 |
+
parser = all_parsers
|
| 266 |
+
dtype = {"b": CategoricalDtype([1, 2, 3])}
|
| 267 |
+
|
| 268 |
+
data = "b\n1\n1\n2\n3"
|
| 269 |
+
expected = DataFrame({"b": Categorical([1, 1, 2, 3])})
|
| 270 |
+
|
| 271 |
+
result = parser.read_csv(StringIO(data), dtype=dtype)
|
| 272 |
+
tm.assert_frame_equal(result, expected)
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
def test_categorical_coerces_datetime(all_parsers):
|
| 276 |
+
parser = all_parsers
|
| 277 |
+
dti = pd.DatetimeIndex(["2017-01-01", "2018-01-01", "2019-01-01"], freq=None)
|
| 278 |
+
dtype = {"b": CategoricalDtype(dti)}
|
| 279 |
+
|
| 280 |
+
data = "b\n2017-01-01\n2018-01-01\n2019-01-01"
|
| 281 |
+
expected = DataFrame({"b": Categorical(dtype["b"].categories)})
|
| 282 |
+
|
| 283 |
+
result = parser.read_csv(StringIO(data), dtype=dtype)
|
| 284 |
+
tm.assert_frame_equal(result, expected)
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
def test_categorical_coerces_timestamp(all_parsers):
|
| 288 |
+
parser = all_parsers
|
| 289 |
+
dtype = {"b": CategoricalDtype([Timestamp("2014")])}
|
| 290 |
+
|
| 291 |
+
data = "b\n2014-01-01\n2014-01-01"
|
| 292 |
+
expected = DataFrame({"b": Categorical([Timestamp("2014")] * 2)})
|
| 293 |
+
|
| 294 |
+
result = parser.read_csv(StringIO(data), dtype=dtype)
|
| 295 |
+
tm.assert_frame_equal(result, expected)
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
def test_categorical_coerces_timedelta(all_parsers):
|
| 299 |
+
parser = all_parsers
|
| 300 |
+
dtype = {"b": CategoricalDtype(pd.to_timedelta(["1h", "2h", "3h"]))}
|
| 301 |
+
|
| 302 |
+
data = "b\n1h\n2h\n3h"
|
| 303 |
+
expected = DataFrame({"b": Categorical(dtype["b"].categories)})
|
| 304 |
+
|
| 305 |
+
result = parser.read_csv(StringIO(data), dtype=dtype)
|
| 306 |
+
tm.assert_frame_equal(result, expected)
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
@pytest.mark.parametrize(
|
| 310 |
+
"data",
|
| 311 |
+
[
|
| 312 |
+
"b\nTrue\nFalse\nNA\nFalse",
|
| 313 |
+
"b\ntrue\nfalse\nNA\nfalse",
|
| 314 |
+
"b\nTRUE\nFALSE\nNA\nFALSE",
|
| 315 |
+
"b\nTrue\nFalse\nNA\nFALSE",
|
| 316 |
+
],
|
| 317 |
+
)
|
| 318 |
+
def test_categorical_dtype_coerces_boolean(all_parsers, data):
|
| 319 |
+
# see gh-20498
|
| 320 |
+
parser = all_parsers
|
| 321 |
+
dtype = {"b": CategoricalDtype([False, True])}
|
| 322 |
+
expected = DataFrame({"b": Categorical([True, False, None, False])})
|
| 323 |
+
|
| 324 |
+
result = parser.read_csv(StringIO(data), dtype=dtype)
|
| 325 |
+
tm.assert_frame_equal(result, expected)
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
def test_categorical_unexpected_categories(all_parsers):
|
| 329 |
+
parser = all_parsers
|
| 330 |
+
dtype = {"b": CategoricalDtype(["a", "b", "d", "e"])}
|
| 331 |
+
|
| 332 |
+
data = "b\nd\na\nc\nd" # Unexpected c
|
| 333 |
+
expected = DataFrame({"b": Categorical(["d", "a", None, "d"], dtype=dtype["b"])})
|
| 334 |
+
|
| 335 |
+
msg = "Constructing a Categorical with a dtype and values containing"
|
| 336 |
+
with tm.assert_produces_warning(Pandas4Warning, match=msg, check_stacklevel=False):
|
| 337 |
+
result = parser.read_csv(StringIO(data), dtype=dtype)
|
| 338 |
+
tm.assert_frame_equal(result, expected)
|
.venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/test_dtypes_basic.py
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tests dtype specification during parsing
|
| 3 |
+
for all of the parsers defined in parsers.py
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from collections import defaultdict
|
| 7 |
+
from io import StringIO
|
| 8 |
+
|
| 9 |
+
import numpy as np
|
| 10 |
+
import pytest
|
| 11 |
+
|
| 12 |
+
from pandas.errors import ParserWarning
|
| 13 |
+
|
| 14 |
+
import pandas as pd
|
| 15 |
+
from pandas import (
|
| 16 |
+
DataFrame,
|
| 17 |
+
Timestamp,
|
| 18 |
+
)
|
| 19 |
+
import pandas._testing as tm
|
| 20 |
+
from pandas.core.arrays import IntegerArray
|
| 21 |
+
|
| 22 |
+
pytestmark = pytest.mark.filterwarnings(
|
| 23 |
+
"ignore:Passing a BlockManager to DataFrame:DeprecationWarning"
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
xfail_pyarrow = pytest.mark.usefixtures("pyarrow_xfail")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@pytest.mark.parametrize("dtype", [str, object])
|
| 30 |
+
@pytest.mark.parametrize("check_orig", [True, False])
|
| 31 |
+
@pytest.mark.usefixtures("pyarrow_xfail")
|
| 32 |
+
def test_dtype_all_columns(
|
| 33 |
+
all_parsers, dtype, check_orig, using_infer_string, temp_file
|
| 34 |
+
):
|
| 35 |
+
# see gh-3795, gh-6607
|
| 36 |
+
parser = all_parsers
|
| 37 |
+
|
| 38 |
+
df = DataFrame(
|
| 39 |
+
np.random.default_rng(2).random((5, 2)).round(4),
|
| 40 |
+
columns=list("AB"),
|
| 41 |
+
index=["1A", "1B", "1C", "1D", "1E"],
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
df.to_csv(temp_file)
|
| 45 |
+
|
| 46 |
+
result = parser.read_csv(temp_file, dtype=dtype, index_col=0)
|
| 47 |
+
|
| 48 |
+
if check_orig:
|
| 49 |
+
expected = df.copy()
|
| 50 |
+
result = result.astype(float)
|
| 51 |
+
elif using_infer_string and dtype is str:
|
| 52 |
+
expected = df.astype(str)
|
| 53 |
+
else:
|
| 54 |
+
expected = df.astype(str).astype(object)
|
| 55 |
+
|
| 56 |
+
tm.assert_frame_equal(result, expected)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@pytest.mark.usefixtures("pyarrow_xfail")
|
| 60 |
+
def test_dtype_per_column(all_parsers):
|
| 61 |
+
parser = all_parsers
|
| 62 |
+
data = """\
|
| 63 |
+
one,two
|
| 64 |
+
1,2.5
|
| 65 |
+
2,3.5
|
| 66 |
+
3,4.5
|
| 67 |
+
4,5.5"""
|
| 68 |
+
expected = DataFrame(
|
| 69 |
+
[[1, "2.5"], [2, "3.5"], [3, "4.5"], [4, "5.5"]], columns=["one", "two"]
|
| 70 |
+
)
|
| 71 |
+
expected["one"] = expected["one"].astype(np.float64)
|
| 72 |
+
|
| 73 |
+
result = parser.read_csv(StringIO(data), dtype={"one": np.float64, 1: str})
|
| 74 |
+
tm.assert_frame_equal(result, expected)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def test_invalid_dtype_per_column(all_parsers):
|
| 78 |
+
parser = all_parsers
|
| 79 |
+
data = """\
|
| 80 |
+
one,two
|
| 81 |
+
1,2.5
|
| 82 |
+
2,3.5
|
| 83 |
+
3,4.5
|
| 84 |
+
4,5.5"""
|
| 85 |
+
|
| 86 |
+
with pytest.raises(TypeError, match="data type [\"']foo[\"'] not understood"):
|
| 87 |
+
parser.read_csv(StringIO(data), dtype={"one": "foo", 1: "int"})
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def test_raise_on_passed_int_dtype_with_nas(all_parsers):
|
| 91 |
+
# see gh-2631
|
| 92 |
+
parser = all_parsers
|
| 93 |
+
data = """YEAR, DOY, a
|
| 94 |
+
2001,106380451,10
|
| 95 |
+
2001,,11
|
| 96 |
+
2001,106380451,67"""
|
| 97 |
+
|
| 98 |
+
if parser.engine == "c":
|
| 99 |
+
msg = "Integer column has NA values"
|
| 100 |
+
elif parser.engine == "pyarrow":
|
| 101 |
+
msg = "The 'skipinitialspace' option is not supported with the 'pyarrow' engine"
|
| 102 |
+
else:
|
| 103 |
+
msg = "Unable to convert column DOY"
|
| 104 |
+
|
| 105 |
+
with pytest.raises(ValueError, match=msg):
|
| 106 |
+
parser.read_csv(StringIO(data), dtype={"DOY": np.int64}, skipinitialspace=True)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def test_dtype_with_converters(all_parsers):
|
| 110 |
+
parser = all_parsers
|
| 111 |
+
data = """a,b
|
| 112 |
+
1.1,2.2
|
| 113 |
+
1.2,2.3"""
|
| 114 |
+
|
| 115 |
+
if parser.engine == "pyarrow":
|
| 116 |
+
msg = "The 'converters' option is not supported with the 'pyarrow' engine"
|
| 117 |
+
with pytest.raises(ValueError, match=msg):
|
| 118 |
+
parser.read_csv(
|
| 119 |
+
StringIO(data), dtype={"a": "i8"}, converters={"a": lambda x: str(x)}
|
| 120 |
+
)
|
| 121 |
+
return
|
| 122 |
+
|
| 123 |
+
# Dtype spec ignored if converted specified.
|
| 124 |
+
result = parser.read_csv_check_warnings(
|
| 125 |
+
ParserWarning,
|
| 126 |
+
"Both a converter and dtype were specified for column a "
|
| 127 |
+
"- only the converter will be used.",
|
| 128 |
+
StringIO(data),
|
| 129 |
+
dtype={"a": "i8"},
|
| 130 |
+
converters={"a": lambda x: str(x)},
|
| 131 |
+
)
|
| 132 |
+
expected = DataFrame({"a": ["1.1", "1.2"], "b": [2.2, 2.3]})
|
| 133 |
+
tm.assert_frame_equal(result, expected)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def test_numeric_dtype(all_parsers, any_real_numpy_dtype):
|
| 137 |
+
data = "0\n1"
|
| 138 |
+
parser = all_parsers
|
| 139 |
+
expected = DataFrame([0, 1], dtype=any_real_numpy_dtype)
|
| 140 |
+
|
| 141 |
+
result = parser.read_csv(StringIO(data), header=None, dtype=any_real_numpy_dtype)
|
| 142 |
+
tm.assert_frame_equal(expected, result, check_column_type=False)
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
@pytest.mark.usefixtures("pyarrow_xfail")
|
| 146 |
+
def test_boolean_dtype(all_parsers):
|
| 147 |
+
parser = all_parsers
|
| 148 |
+
data = "\n".join(
|
| 149 |
+
[
|
| 150 |
+
"a",
|
| 151 |
+
"True",
|
| 152 |
+
"TRUE",
|
| 153 |
+
"true",
|
| 154 |
+
"1",
|
| 155 |
+
"1.0",
|
| 156 |
+
"False",
|
| 157 |
+
"FALSE",
|
| 158 |
+
"false",
|
| 159 |
+
"0",
|
| 160 |
+
"0.0",
|
| 161 |
+
"NaN",
|
| 162 |
+
"nan",
|
| 163 |
+
"NA",
|
| 164 |
+
"null",
|
| 165 |
+
"NULL",
|
| 166 |
+
]
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
result = parser.read_csv(StringIO(data), dtype="boolean")
|
| 170 |
+
expected = DataFrame(
|
| 171 |
+
{
|
| 172 |
+
"a": pd.array(
|
| 173 |
+
[
|
| 174 |
+
True,
|
| 175 |
+
True,
|
| 176 |
+
True,
|
| 177 |
+
True,
|
| 178 |
+
True,
|
| 179 |
+
False,
|
| 180 |
+
False,
|
| 181 |
+
False,
|
| 182 |
+
False,
|
| 183 |
+
False,
|
| 184 |
+
None,
|
| 185 |
+
None,
|
| 186 |
+
None,
|
| 187 |
+
None,
|
| 188 |
+
None,
|
| 189 |
+
],
|
| 190 |
+
dtype="boolean",
|
| 191 |
+
)
|
| 192 |
+
}
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
tm.assert_frame_equal(result, expected)
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
@pytest.mark.usefixtures("pyarrow_xfail")
|
| 199 |
+
def test_delimiter_with_usecols_and_parse_dates(all_parsers):
|
| 200 |
+
# GH#35873
|
| 201 |
+
result = all_parsers.read_csv(
|
| 202 |
+
StringIO('"dump","-9,1","-9,1",20101010'),
|
| 203 |
+
engine="python",
|
| 204 |
+
names=["col", "col1", "col2", "col3"],
|
| 205 |
+
usecols=["col1", "col2", "col3"],
|
| 206 |
+
parse_dates=["col3"],
|
| 207 |
+
decimal=",",
|
| 208 |
+
)
|
| 209 |
+
expected = DataFrame(
|
| 210 |
+
{"col1": [-9.1], "col2": [-9.1], "col3": [Timestamp("2010-10-10")]}
|
| 211 |
+
)
|
| 212 |
+
tm.assert_frame_equal(result, expected)
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
@pytest.mark.parametrize("thousands", ["_", None])
|
| 216 |
+
def test_decimal_and_exponential(
|
| 217 |
+
request, python_parser_only, numeric_decimal, thousands
|
| 218 |
+
):
|
| 219 |
+
# GH#31920
|
| 220 |
+
decimal_number_check(request, python_parser_only, numeric_decimal, thousands, None)
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
@pytest.mark.parametrize("thousands", ["_", None])
|
| 224 |
+
@pytest.mark.parametrize("float_precision", [None, "legacy", "high", "round_trip"])
|
| 225 |
+
def test_1000_sep_decimal_float_precision(
|
| 226 |
+
request, c_parser_only, numeric_decimal, float_precision, thousands
|
| 227 |
+
):
|
| 228 |
+
# test decimal and thousand sep handling in across 'float_precision'
|
| 229 |
+
# parsers
|
| 230 |
+
decimal_number_check(
|
| 231 |
+
request, c_parser_only, numeric_decimal, thousands, float_precision
|
| 232 |
+
)
|
| 233 |
+
text, value = numeric_decimal
|
| 234 |
+
text = " " + text + " "
|
| 235 |
+
if isinstance(value, str): # the negative cases (parse as text)
|
| 236 |
+
value = " " + value + " "
|
| 237 |
+
decimal_number_check(
|
| 238 |
+
request, c_parser_only, (text, value), thousands, float_precision
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def decimal_number_check(request, parser, numeric_decimal, thousands, float_precision):
|
| 243 |
+
# GH#31920
|
| 244 |
+
value = numeric_decimal[0]
|
| 245 |
+
if thousands is None and value in ("1_,", "1_234,56", "1_234,56e0"):
|
| 246 |
+
request.applymarker(
|
| 247 |
+
pytest.mark.xfail(reason=f"thousands={thousands} and sep is in {value}")
|
| 248 |
+
)
|
| 249 |
+
df = parser.read_csv(
|
| 250 |
+
StringIO(value),
|
| 251 |
+
float_precision=float_precision,
|
| 252 |
+
sep="|",
|
| 253 |
+
thousands=thousands,
|
| 254 |
+
decimal=",",
|
| 255 |
+
header=None,
|
| 256 |
+
)
|
| 257 |
+
val = df.iloc[0, 0]
|
| 258 |
+
assert val == numeric_decimal[1]
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
@pytest.mark.parametrize("float_precision", [None, "legacy", "high", "round_trip"])
|
| 262 |
+
def test_skip_whitespace(c_parser_only, float_precision):
|
| 263 |
+
DATA = """id\tnum\t
|
| 264 |
+
1\t1.2 \t
|
| 265 |
+
1\t 2.1\t
|
| 266 |
+
2\t 1\t
|
| 267 |
+
2\t 1.2 \t
|
| 268 |
+
"""
|
| 269 |
+
df = c_parser_only.read_csv(
|
| 270 |
+
StringIO(DATA),
|
| 271 |
+
float_precision=float_precision,
|
| 272 |
+
sep="\t",
|
| 273 |
+
header=0,
|
| 274 |
+
dtype={1: np.float64},
|
| 275 |
+
)
|
| 276 |
+
tm.assert_series_equal(df.iloc[:, 1], pd.Series([1.2, 2.1, 1.0, 1.2], name="num"))
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
@pytest.mark.usefixtures("pyarrow_xfail")
|
| 280 |
+
def test_true_values_cast_to_bool(all_parsers):
|
| 281 |
+
# GH#34655
|
| 282 |
+
text = """a,b
|
| 283 |
+
yes,xxx
|
| 284 |
+
no,yyy
|
| 285 |
+
1,zzz
|
| 286 |
+
0,aaa
|
| 287 |
+
"""
|
| 288 |
+
parser = all_parsers
|
| 289 |
+
result = parser.read_csv(
|
| 290 |
+
StringIO(text),
|
| 291 |
+
true_values=["yes"],
|
| 292 |
+
false_values=["no"],
|
| 293 |
+
dtype={"a": "boolean"},
|
| 294 |
+
)
|
| 295 |
+
expected = DataFrame(
|
| 296 |
+
{"a": [True, False, True, False], "b": ["xxx", "yyy", "zzz", "aaa"]}
|
| 297 |
+
)
|
| 298 |
+
expected["a"] = expected["a"].astype("boolean")
|
| 299 |
+
tm.assert_frame_equal(result, expected)
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
@pytest.mark.usefixtures("pyarrow_xfail")
|
| 303 |
+
@pytest.mark.parametrize("dtypes, exp_value", [({}, "1"), ({"a.1": "int64"}, 1)])
|
| 304 |
+
def test_dtype_mangle_dup_cols(all_parsers, dtypes, exp_value):
|
| 305 |
+
# GH#35211
|
| 306 |
+
parser = all_parsers
|
| 307 |
+
data = """a,a\n1,1"""
|
| 308 |
+
dtype_dict = {"a": str, **dtypes}
|
| 309 |
+
# GH#42462
|
| 310 |
+
dtype_dict_copy = dtype_dict.copy()
|
| 311 |
+
result = parser.read_csv(StringIO(data), dtype=dtype_dict)
|
| 312 |
+
expected = DataFrame({"a": ["1"], "a.1": [exp_value]})
|
| 313 |
+
assert dtype_dict == dtype_dict_copy, "dtype dict changed"
|
| 314 |
+
tm.assert_frame_equal(result, expected)
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
@pytest.mark.usefixtures("pyarrow_xfail")
|
| 318 |
+
def test_dtype_mangle_dup_cols_single_dtype(all_parsers):
|
| 319 |
+
# GH#42022
|
| 320 |
+
parser = all_parsers
|
| 321 |
+
data = """a,a\n1,1"""
|
| 322 |
+
result = parser.read_csv(StringIO(data), dtype=str)
|
| 323 |
+
expected = DataFrame({"a": ["1"], "a.1": ["1"]})
|
| 324 |
+
tm.assert_frame_equal(result, expected)
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
@pytest.mark.usefixtures("pyarrow_xfail")
|
| 328 |
+
def test_dtype_multi_index(all_parsers):
|
| 329 |
+
# GH 42446
|
| 330 |
+
parser = all_parsers
|
| 331 |
+
data = "A,B,B\nX,Y,Z\n1,2,3"
|
| 332 |
+
|
| 333 |
+
result = parser.read_csv(
|
| 334 |
+
StringIO(data),
|
| 335 |
+
header=list(range(2)),
|
| 336 |
+
dtype={
|
| 337 |
+
("A", "X"): np.int32,
|
| 338 |
+
("B", "Y"): np.int32,
|
| 339 |
+
("B", "Z"): np.float32,
|
| 340 |
+
},
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
expected = DataFrame(
|
| 344 |
+
{
|
| 345 |
+
("A", "X"): np.int32([1]),
|
| 346 |
+
("B", "Y"): np.int32([2]),
|
| 347 |
+
("B", "Z"): np.float32([3]),
|
| 348 |
+
}
|
| 349 |
+
)
|
| 350 |
+
|
| 351 |
+
tm.assert_frame_equal(result, expected)
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
def test_nullable_int_dtype(all_parsers, any_int_ea_dtype):
|
| 355 |
+
# GH 25472
|
| 356 |
+
parser = all_parsers
|
| 357 |
+
dtype = any_int_ea_dtype
|
| 358 |
+
|
| 359 |
+
data = """a,b,c
|
| 360 |
+
,3,5
|
| 361 |
+
1,,6
|
| 362 |
+
2,4,"""
|
| 363 |
+
expected = DataFrame(
|
| 364 |
+
{
|
| 365 |
+
"a": pd.array([pd.NA, 1, 2], dtype=dtype),
|
| 366 |
+
"b": pd.array([3, pd.NA, 4], dtype=dtype),
|
| 367 |
+
"c": pd.array([5, 6, pd.NA], dtype=dtype),
|
| 368 |
+
}
|
| 369 |
+
)
|
| 370 |
+
actual = parser.read_csv(StringIO(data), dtype=dtype)
|
| 371 |
+
tm.assert_frame_equal(actual, expected)
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
@pytest.mark.usefixtures("pyarrow_xfail")
|
| 375 |
+
@pytest.mark.parametrize("default", ["float", "float64"])
|
| 376 |
+
def test_dtypes_defaultdict(all_parsers, default):
|
| 377 |
+
# GH#41574
|
| 378 |
+
data = """a,b
|
| 379 |
+
1,2
|
| 380 |
+
"""
|
| 381 |
+
dtype = defaultdict(lambda: default, a="int64")
|
| 382 |
+
parser = all_parsers
|
| 383 |
+
result = parser.read_csv(StringIO(data), dtype=dtype)
|
| 384 |
+
expected = DataFrame({"a": [1], "b": 2.0})
|
| 385 |
+
tm.assert_frame_equal(result, expected)
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
@pytest.mark.usefixtures("pyarrow_xfail")
|
| 389 |
+
def test_dtypes_defaultdict_mangle_dup_cols(all_parsers):
|
| 390 |
+
# GH#41574
|
| 391 |
+
data = """a,b,a,b,b.1
|
| 392 |
+
1,2,3,4,5
|
| 393 |
+
"""
|
| 394 |
+
dtype = defaultdict(lambda: "float64", a="int64")
|
| 395 |
+
dtype["b.1"] = "int64"
|
| 396 |
+
parser = all_parsers
|
| 397 |
+
result = parser.read_csv(StringIO(data), dtype=dtype)
|
| 398 |
+
expected = DataFrame({"a": [1], "b": [2.0], "a.1": [3], "b.2": [4.0], "b.1": [5]})
|
| 399 |
+
tm.assert_frame_equal(result, expected)
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
@pytest.mark.usefixtures("pyarrow_xfail")
|
| 403 |
+
def test_dtypes_defaultdict_invalid(all_parsers):
|
| 404 |
+
# GH#41574
|
| 405 |
+
data = """a,b
|
| 406 |
+
1,2
|
| 407 |
+
"""
|
| 408 |
+
dtype = defaultdict(lambda: "invalid_dtype", a="int64")
|
| 409 |
+
parser = all_parsers
|
| 410 |
+
with pytest.raises(TypeError, match="not understood"):
|
| 411 |
+
parser.read_csv(StringIO(data), dtype=dtype)
|
| 412 |
+
|
| 413 |
+
|
| 414 |
+
def test_dtype_backend(all_parsers):
|
| 415 |
+
# GH#36712
|
| 416 |
+
|
| 417 |
+
parser = all_parsers
|
| 418 |
+
|
| 419 |
+
data = """a,b,c,d,e,f,g,h,i,j
|
| 420 |
+
1,2.5,True,a,,,,,12-31-2019,
|
| 421 |
+
3,4.5,False,b,6,7.5,True,a,12-31-2019,
|
| 422 |
+
"""
|
| 423 |
+
result = parser.read_csv(
|
| 424 |
+
StringIO(data), dtype_backend="numpy_nullable", parse_dates=["i"]
|
| 425 |
+
)
|
| 426 |
+
expected = DataFrame(
|
| 427 |
+
{
|
| 428 |
+
"a": pd.Series([1, 3], dtype="Int64"),
|
| 429 |
+
"b": pd.Series([2.5, 4.5], dtype="Float64"),
|
| 430 |
+
"c": pd.Series([True, False], dtype="boolean"),
|
| 431 |
+
"d": pd.Series(["a", "b"], dtype="string"),
|
| 432 |
+
"e": pd.Series([pd.NA, 6], dtype="Int64"),
|
| 433 |
+
"f": pd.Series([pd.NA, 7.5], dtype="Float64"),
|
| 434 |
+
"g": pd.Series([pd.NA, True], dtype="boolean"),
|
| 435 |
+
"h": pd.Series([pd.NA, "a"], dtype="string"),
|
| 436 |
+
"i": pd.Series([Timestamp("2019-12-31")] * 2),
|
| 437 |
+
"j": pd.Series([pd.NA, pd.NA], dtype="Int64"),
|
| 438 |
+
}
|
| 439 |
+
)
|
| 440 |
+
tm.assert_frame_equal(result, expected)
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
def test_dtype_backend_and_dtype(all_parsers):
|
| 444 |
+
# GH#36712
|
| 445 |
+
|
| 446 |
+
parser = all_parsers
|
| 447 |
+
|
| 448 |
+
data = """a,b
|
| 449 |
+
1,2.5
|
| 450 |
+
,
|
| 451 |
+
"""
|
| 452 |
+
result = parser.read_csv(
|
| 453 |
+
StringIO(data), dtype_backend="numpy_nullable", dtype="float64"
|
| 454 |
+
)
|
| 455 |
+
expected = DataFrame({"a": [1.0, np.nan], "b": [2.5, np.nan]})
|
| 456 |
+
tm.assert_frame_equal(result, expected)
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
def test_dtype_backend_string(all_parsers, string_storage):
|
| 460 |
+
# GH#36712
|
| 461 |
+
with pd.option_context("mode.string_storage", string_storage):
|
| 462 |
+
parser = all_parsers
|
| 463 |
+
|
| 464 |
+
data = """a,b
|
| 465 |
+
a,x
|
| 466 |
+
b,
|
| 467 |
+
"""
|
| 468 |
+
result = parser.read_csv(StringIO(data), dtype_backend="numpy_nullable")
|
| 469 |
+
|
| 470 |
+
expected = DataFrame(
|
| 471 |
+
{
|
| 472 |
+
"a": pd.array(["a", "b"], dtype=pd.StringDtype(string_storage)),
|
| 473 |
+
"b": pd.array(["x", pd.NA], dtype=pd.StringDtype(string_storage)),
|
| 474 |
+
},
|
| 475 |
+
)
|
| 476 |
+
tm.assert_frame_equal(result, expected)
|
| 477 |
+
|
| 478 |
+
|
| 479 |
+
def test_dtype_backend_ea_dtype_specified(all_parsers):
|
| 480 |
+
# GH#491496
|
| 481 |
+
data = """a,b
|
| 482 |
+
1,2
|
| 483 |
+
"""
|
| 484 |
+
parser = all_parsers
|
| 485 |
+
result = parser.read_csv(
|
| 486 |
+
StringIO(data), dtype="Int64", dtype_backend="numpy_nullable"
|
| 487 |
+
)
|
| 488 |
+
expected = DataFrame({"a": [1], "b": 2}, dtype="Int64")
|
| 489 |
+
tm.assert_frame_equal(result, expected)
|
| 490 |
+
|
| 491 |
+
|
| 492 |
+
def test_dtype_backend_pyarrow(all_parsers, request):
|
| 493 |
+
# GH#36712
|
| 494 |
+
pa = pytest.importorskip("pyarrow")
|
| 495 |
+
parser = all_parsers
|
| 496 |
+
|
| 497 |
+
data = """a,b,c,d,e,f,g,h,i,j
|
| 498 |
+
1,2.5,True,a,,,,,12-31-2019,
|
| 499 |
+
3,4.5,False,b,6,7.5,True,a,12-31-2019,
|
| 500 |
+
"""
|
| 501 |
+
result = parser.read_csv(StringIO(data), dtype_backend="pyarrow", parse_dates=["i"])
|
| 502 |
+
expected = DataFrame(
|
| 503 |
+
{
|
| 504 |
+
"a": pd.Series([1, 3], dtype="int64[pyarrow]"),
|
| 505 |
+
"b": pd.Series([2.5, 4.5], dtype="float64[pyarrow]"),
|
| 506 |
+
"c": pd.Series([True, False], dtype="bool[pyarrow]"),
|
| 507 |
+
"d": pd.Series(["a", "b"], dtype=pd.ArrowDtype(pa.string())),
|
| 508 |
+
"e": pd.Series([pd.NA, 6], dtype="int64[pyarrow]"),
|
| 509 |
+
"f": pd.Series([pd.NA, 7.5], dtype="float64[pyarrow]"),
|
| 510 |
+
"g": pd.Series([pd.NA, True], dtype="bool[pyarrow]"),
|
| 511 |
+
"h": pd.Series(
|
| 512 |
+
[pd.NA, "a"],
|
| 513 |
+
dtype=pd.ArrowDtype(pa.string()),
|
| 514 |
+
),
|
| 515 |
+
"i": pd.Series([Timestamp("2019-12-31")] * 2),
|
| 516 |
+
"j": pd.Series([pd.NA, pd.NA], dtype="null[pyarrow]"),
|
| 517 |
+
}
|
| 518 |
+
)
|
| 519 |
+
tm.assert_frame_equal(result, expected)
|
| 520 |
+
|
| 521 |
+
|
| 522 |
+
def test_ea_int_avoid_overflow(all_parsers):
|
| 523 |
+
# GH#32134
|
| 524 |
+
parser = all_parsers
|
| 525 |
+
data = """a,b
|
| 526 |
+
1,1
|
| 527 |
+
,1
|
| 528 |
+
1582218195625938945,1
|
| 529 |
+
"""
|
| 530 |
+
result = parser.read_csv(StringIO(data), dtype={"a": "Int64"})
|
| 531 |
+
expected = DataFrame(
|
| 532 |
+
{
|
| 533 |
+
"a": IntegerArray(
|
| 534 |
+
np.array([1, 1, 1582218195625938945]), np.array([False, True, False])
|
| 535 |
+
),
|
| 536 |
+
"b": 1,
|
| 537 |
+
}
|
| 538 |
+
)
|
| 539 |
+
tm.assert_frame_equal(result, expected)
|
| 540 |
+
|
| 541 |
+
|
| 542 |
+
def test_string_inference(all_parsers):
|
| 543 |
+
# GH#54430
|
| 544 |
+
dtype = pd.StringDtype(na_value=np.nan)
|
| 545 |
+
|
| 546 |
+
data = """a,b
|
| 547 |
+
x,1
|
| 548 |
+
y,2
|
| 549 |
+
,3"""
|
| 550 |
+
parser = all_parsers
|
| 551 |
+
with pd.option_context("future.infer_string", True):
|
| 552 |
+
result = parser.read_csv(StringIO(data))
|
| 553 |
+
|
| 554 |
+
expected = DataFrame(
|
| 555 |
+
{"a": pd.Series(["x", "y", None], dtype=dtype), "b": [1, 2, 3]},
|
| 556 |
+
columns=pd.Index(["a", "b"], dtype=dtype),
|
| 557 |
+
)
|
| 558 |
+
tm.assert_frame_equal(result, expected)
|
| 559 |
+
|
| 560 |
+
|
| 561 |
+
@pytest.mark.parametrize("dtype", ["O", object, "object", np.object_, str, np.str_])
|
| 562 |
+
def test_string_inference_object_dtype(all_parsers, dtype, using_infer_string):
|
| 563 |
+
# GH#56047
|
| 564 |
+
data = """a,b
|
| 565 |
+
x,a
|
| 566 |
+
y,a
|
| 567 |
+
z,a"""
|
| 568 |
+
parser = all_parsers
|
| 569 |
+
with pd.option_context("future.infer_string", True):
|
| 570 |
+
result = parser.read_csv(StringIO(data), dtype=dtype)
|
| 571 |
+
|
| 572 |
+
expected_dtype = pd.StringDtype(na_value=np.nan) if dtype is str else object
|
| 573 |
+
expected = DataFrame(
|
| 574 |
+
{
|
| 575 |
+
"a": pd.Series(["x", "y", "z"], dtype=expected_dtype),
|
| 576 |
+
"b": pd.Series(["a", "a", "a"], dtype=expected_dtype),
|
| 577 |
+
},
|
| 578 |
+
columns=pd.Index(["a", "b"], dtype=pd.StringDtype(na_value=np.nan)),
|
| 579 |
+
)
|
| 580 |
+
tm.assert_frame_equal(result, expected)
|
| 581 |
+
|
| 582 |
+
with pd.option_context("future.infer_string", True):
|
| 583 |
+
result = parser.read_csv(StringIO(data), dtype={"a": dtype})
|
| 584 |
+
|
| 585 |
+
expected = DataFrame(
|
| 586 |
+
{
|
| 587 |
+
"a": pd.Series(["x", "y", "z"], dtype=expected_dtype),
|
| 588 |
+
"b": pd.Series(["a", "a", "a"], dtype=pd.StringDtype(na_value=np.nan)),
|
| 589 |
+
},
|
| 590 |
+
columns=pd.Index(["a", "b"], dtype=pd.StringDtype(na_value=np.nan)),
|
| 591 |
+
)
|
| 592 |
+
tm.assert_frame_equal(result, expected)
|
| 593 |
+
|
| 594 |
+
|
| 595 |
+
def test_accurate_parsing_of_large_integers(all_parsers):
|
| 596 |
+
# GH#52505
|
| 597 |
+
data = """SYMBOL,MOMENT,ID,ID_DEAL
|
| 598 |
+
AAPL,20230301181139587,1925036343869802844,
|
| 599 |
+
AAPL,20230301181139587,2023552585717889863,2023552585717263358
|
| 600 |
+
NVDA,20230301181139587,2023552585717889863,2023552585717263359
|
| 601 |
+
AMC,20230301181139587,2023552585717889863,2023552585717263360
|
| 602 |
+
AMZN,20230301181139587,2023552585717889759,2023552585717263360
|
| 603 |
+
MSFT,20230301181139587,2023552585717889863,2023552585717263361
|
| 604 |
+
NVDA,20230301181139587,2023552585717889827,2023552585717263361"""
|
| 605 |
+
orders = all_parsers.read_csv(StringIO(data), dtype={"ID_DEAL": pd.Int64Dtype()})
|
| 606 |
+
assert len(orders.loc[orders["ID_DEAL"] == 2023552585717263358, "ID_DEAL"]) == 1
|
| 607 |
+
assert len(orders.loc[orders["ID_DEAL"] == 2023552585717263359, "ID_DEAL"]) == 1
|
| 608 |
+
assert len(orders.loc[orders["ID_DEAL"] == 2023552585717263360, "ID_DEAL"]) == 2
|
| 609 |
+
assert len(orders.loc[orders["ID_DEAL"] == 2023552585717263361, "ID_DEAL"]) == 2
|
| 610 |
+
|
| 611 |
+
|
| 612 |
+
def test_dtypes_with_usecols(all_parsers):
|
| 613 |
+
# GH#54868
|
| 614 |
+
|
| 615 |
+
parser = all_parsers
|
| 616 |
+
data = """a,b,c
|
| 617 |
+
1,2,3
|
| 618 |
+
4,5,6"""
|
| 619 |
+
|
| 620 |
+
result = parser.read_csv(StringIO(data), usecols=["a", "c"], dtype={"a": object})
|
| 621 |
+
if parser.engine == "pyarrow":
|
| 622 |
+
values = [1, 4]
|
| 623 |
+
else:
|
| 624 |
+
values = ["1", "4"]
|
| 625 |
+
expected = DataFrame({"a": pd.Series(values, dtype=object), "c": [3, 6]})
|
| 626 |
+
tm.assert_frame_equal(result, expected)
|
| 627 |
+
|
| 628 |
+
|
| 629 |
+
def test_index_col_with_dtype_no_rangeindex(all_parsers):
|
| 630 |
+
data = StringIO("345.5,519.5,0\n519.5,726.5,1")
|
| 631 |
+
result = all_parsers.read_csv(
|
| 632 |
+
data,
|
| 633 |
+
header=None,
|
| 634 |
+
names=["start", "stop", "bin_id"],
|
| 635 |
+
dtype={"start": np.float32, "stop": np.float32, "bin_id": np.uint32},
|
| 636 |
+
index_col="bin_id",
|
| 637 |
+
).index
|
| 638 |
+
expected = pd.Index([0, 1], dtype=np.uint32, name="bin_id")
|
| 639 |
+
tm.assert_index_equal(result, expected)
|
.venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/test_empty.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tests dtype specification during parsing
|
| 3 |
+
for all of the parsers defined in parsers.py
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from io import StringIO
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import pytest
|
| 10 |
+
|
| 11 |
+
from pandas import (
|
| 12 |
+
Categorical,
|
| 13 |
+
DataFrame,
|
| 14 |
+
Index,
|
| 15 |
+
MultiIndex,
|
| 16 |
+
Series,
|
| 17 |
+
concat,
|
| 18 |
+
)
|
| 19 |
+
import pandas._testing as tm
|
| 20 |
+
|
| 21 |
+
skip_pyarrow = pytest.mark.usefixtures("pyarrow_skip")
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@skip_pyarrow # CSV parse error: Empty CSV file or block
|
| 25 |
+
def test_dtype_all_columns_empty(all_parsers):
|
| 26 |
+
# see gh-12048
|
| 27 |
+
parser = all_parsers
|
| 28 |
+
result = parser.read_csv(StringIO("A,B"), dtype=str)
|
| 29 |
+
|
| 30 |
+
expected = DataFrame({"A": [], "B": []}, dtype=str)
|
| 31 |
+
tm.assert_frame_equal(result, expected)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@skip_pyarrow # CSV parse error: Empty CSV file or block
|
| 35 |
+
def test_empty_pass_dtype(all_parsers):
|
| 36 |
+
parser = all_parsers
|
| 37 |
+
|
| 38 |
+
data = "one,two"
|
| 39 |
+
result = parser.read_csv(StringIO(data), dtype={"one": "u1"})
|
| 40 |
+
|
| 41 |
+
expected = DataFrame(
|
| 42 |
+
{"one": np.empty(0, dtype="u1"), "two": np.empty(0, dtype=object)},
|
| 43 |
+
)
|
| 44 |
+
tm.assert_frame_equal(result, expected)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@skip_pyarrow # CSV parse error: Empty CSV file or block
|
| 48 |
+
def test_empty_with_index_pass_dtype(all_parsers):
|
| 49 |
+
parser = all_parsers
|
| 50 |
+
|
| 51 |
+
data = "one,two"
|
| 52 |
+
result = parser.read_csv(
|
| 53 |
+
StringIO(data), index_col=["one"], dtype={"one": "u1", 1: "f"}
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
expected = DataFrame(
|
| 57 |
+
{"two": np.empty(0, dtype="f")}, index=Index([], dtype="u1", name="one")
|
| 58 |
+
)
|
| 59 |
+
tm.assert_frame_equal(result, expected)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
@skip_pyarrow # CSV parse error: Empty CSV file or block
|
| 63 |
+
def test_empty_with_multi_index_pass_dtype(all_parsers):
|
| 64 |
+
parser = all_parsers
|
| 65 |
+
|
| 66 |
+
data = "one,two,three"
|
| 67 |
+
result = parser.read_csv(
|
| 68 |
+
StringIO(data), index_col=["one", "two"], dtype={"one": "u1", 1: "f8"}
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
exp_idx = MultiIndex.from_arrays(
|
| 72 |
+
[np.empty(0, dtype="u1"), np.empty(0, dtype=np.float64)],
|
| 73 |
+
names=["one", "two"],
|
| 74 |
+
)
|
| 75 |
+
expected = DataFrame({"three": np.empty(0, dtype=object)}, index=exp_idx)
|
| 76 |
+
tm.assert_frame_equal(result, expected)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
@skip_pyarrow # CSV parse error: Empty CSV file or block
|
| 80 |
+
def test_empty_with_mangled_column_pass_dtype_by_names(all_parsers):
|
| 81 |
+
parser = all_parsers
|
| 82 |
+
|
| 83 |
+
data = "one,one"
|
| 84 |
+
result = parser.read_csv(StringIO(data), dtype={"one": "u1", "one.1": "f"})
|
| 85 |
+
|
| 86 |
+
expected = DataFrame(
|
| 87 |
+
{"one": np.empty(0, dtype="u1"), "one.1": np.empty(0, dtype="f")},
|
| 88 |
+
)
|
| 89 |
+
tm.assert_frame_equal(result, expected)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
@skip_pyarrow # CSV parse error: Empty CSV file or block
|
| 93 |
+
def test_empty_with_mangled_column_pass_dtype_by_indexes(all_parsers):
|
| 94 |
+
parser = all_parsers
|
| 95 |
+
|
| 96 |
+
data = "one,one"
|
| 97 |
+
result = parser.read_csv(StringIO(data), dtype={0: "u1", 1: "f"})
|
| 98 |
+
|
| 99 |
+
expected = DataFrame(
|
| 100 |
+
{"one": np.empty(0, dtype="u1"), "one.1": np.empty(0, dtype="f")},
|
| 101 |
+
)
|
| 102 |
+
tm.assert_frame_equal(result, expected)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
@skip_pyarrow # CSV parse error: Empty CSV file or block
|
| 106 |
+
def test_empty_with_dup_column_pass_dtype_by_indexes(all_parsers):
|
| 107 |
+
# see gh-9424
|
| 108 |
+
parser = all_parsers
|
| 109 |
+
expected = concat(
|
| 110 |
+
[Series([], name="one", dtype="u1"), Series([], name="one.1", dtype="f")],
|
| 111 |
+
axis=1,
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
data = "one,one"
|
| 115 |
+
result = parser.read_csv(StringIO(data), dtype={0: "u1", 1: "f"})
|
| 116 |
+
tm.assert_frame_equal(result, expected)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def test_empty_with_dup_column_pass_dtype_by_indexes_raises(all_parsers):
|
| 120 |
+
# see gh-9424
|
| 121 |
+
parser = all_parsers
|
| 122 |
+
expected = concat(
|
| 123 |
+
[Series([], name="one", dtype="u1"), Series([], name="one.1", dtype="f")],
|
| 124 |
+
axis=1,
|
| 125 |
+
)
|
| 126 |
+
expected.index = expected.index.astype(object)
|
| 127 |
+
|
| 128 |
+
with pytest.raises(ValueError, match="Duplicate names"):
|
| 129 |
+
parser.read_csv(StringIO(""), names=["one", "one"], dtype={0: "u1", 1: "f"})
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
@pytest.mark.parametrize(
|
| 133 |
+
"dtype,expected",
|
| 134 |
+
[
|
| 135 |
+
(np.float64, DataFrame(columns=["a", "b"], dtype=np.float64)),
|
| 136 |
+
(
|
| 137 |
+
"category",
|
| 138 |
+
DataFrame({"a": Categorical([]), "b": Categorical([])}),
|
| 139 |
+
),
|
| 140 |
+
(
|
| 141 |
+
{"a": "category", "b": "category"},
|
| 142 |
+
DataFrame({"a": Categorical([]), "b": Categorical([])}),
|
| 143 |
+
),
|
| 144 |
+
("datetime64[ns]", DataFrame(columns=["a", "b"], dtype="datetime64[ns]")),
|
| 145 |
+
(
|
| 146 |
+
"timedelta64[ns]",
|
| 147 |
+
DataFrame(
|
| 148 |
+
{
|
| 149 |
+
"a": Series([], dtype="timedelta64[ns]"),
|
| 150 |
+
"b": Series([], dtype="timedelta64[ns]"),
|
| 151 |
+
},
|
| 152 |
+
),
|
| 153 |
+
),
|
| 154 |
+
(
|
| 155 |
+
{"a": np.int64, "b": np.int32},
|
| 156 |
+
DataFrame(
|
| 157 |
+
{"a": Series([], dtype=np.int64), "b": Series([], dtype=np.int32)},
|
| 158 |
+
),
|
| 159 |
+
),
|
| 160 |
+
(
|
| 161 |
+
{0: np.int64, 1: np.int32},
|
| 162 |
+
DataFrame(
|
| 163 |
+
{"a": Series([], dtype=np.int64), "b": Series([], dtype=np.int32)},
|
| 164 |
+
),
|
| 165 |
+
),
|
| 166 |
+
(
|
| 167 |
+
{"a": np.int64, 1: np.int32},
|
| 168 |
+
DataFrame(
|
| 169 |
+
{"a": Series([], dtype=np.int64), "b": Series([], dtype=np.int32)},
|
| 170 |
+
),
|
| 171 |
+
),
|
| 172 |
+
],
|
| 173 |
+
)
|
| 174 |
+
@skip_pyarrow # CSV parse error: Empty CSV file or block
|
| 175 |
+
def test_empty_dtype(all_parsers, dtype, expected):
|
| 176 |
+
# see gh-14712
|
| 177 |
+
parser = all_parsers
|
| 178 |
+
data = "a,b"
|
| 179 |
+
|
| 180 |
+
result = parser.read_csv(StringIO(data), header=0, dtype=dtype)
|
| 181 |
+
tm.assert_frame_equal(result, expected)
|