Brettapps commited on
Commit
44d1dff
·
verified ·
1 Parent(s): 66180c2

Upload folder using huggingface_hub (part 13)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_highlight.py +221 -0
  2. .venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_html.py +1038 -0
  3. .venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_matplotlib.py +305 -0
  4. .venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_non_unique.py +140 -0
  5. .venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_style.py +1602 -0
  6. .venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_to_latex.py +1091 -0
  7. .venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_to_string.py +96 -0
  8. .venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_to_typst.py +96 -0
  9. .venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_tooltip.py +179 -0
  10. .venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_console.py +72 -0
  11. .venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_css.py +288 -0
  12. .venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_eng_formatting.py +254 -0
  13. .venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_format.py +2302 -0
  14. .venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_ipython_compat.py +91 -0
  15. .venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_printing.py +172 -0
  16. .venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_csv.py +886 -0
  17. .venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_excel.py +473 -0
  18. .venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_html.py +1166 -0
  19. .venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_latex.py +1527 -0
  20. .venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_markdown.py +102 -0
  21. .venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_string.py +1226 -0
  22. .venv/lib/python3.13/site-packages/pandas/tests/io/generate_legacy_storage_files.py +421 -0
  23. .venv/lib/python3.13/site-packages/pandas/tests/io/json/__init__.py +0 -0
  24. .venv/lib/python3.13/site-packages/pandas/tests/io/json/conftest.py +9 -0
  25. .venv/lib/python3.13/site-packages/pandas/tests/io/json/test_compression.py +130 -0
  26. .venv/lib/python3.13/site-packages/pandas/tests/io/json/test_deprecated_kwargs.py +22 -0
  27. .venv/lib/python3.13/site-packages/pandas/tests/io/json/test_json_table_schema.py +898 -0
  28. .venv/lib/python3.13/site-packages/pandas/tests/io/json/test_json_table_schema_ext_dtype.py +296 -0
  29. .venv/lib/python3.13/site-packages/pandas/tests/io/json/test_normalize.py +942 -0
  30. .venv/lib/python3.13/site-packages/pandas/tests/io/json/test_pandas.py +2387 -0
  31. .venv/lib/python3.13/site-packages/pandas/tests/io/json/test_readlines.py +544 -0
  32. .venv/lib/python3.13/site-packages/pandas/tests/io/json/test_ujson.py +1045 -0
  33. .venv/lib/python3.13/site-packages/pandas/tests/io/parser/__init__.py +0 -0
  34. .venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/__init__.py +0 -0
  35. .venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_chunksize.py +364 -0
  36. .venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_common_basic.py +868 -0
  37. .venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_data_list.py +92 -0
  38. .venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_decimal.py +73 -0
  39. .venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_file_buffer_url.py +470 -0
  40. .venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_float.py +110 -0
  41. .venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_index.py +307 -0
  42. .venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_inf.py +59 -0
  43. .venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_ints.py +244 -0
  44. .venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_iterator.py +159 -0
  45. .venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_read_errors.py +315 -0
  46. .venv/lib/python3.13/site-packages/pandas/tests/io/parser/conftest.py +337 -0
  47. .venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/__init__.py +0 -0
  48. .venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/test_categorical.py +338 -0
  49. .venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/test_dtypes_basic.py +639 -0
  50. .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 >&nbsp;</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" >&nbsp;</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" >&nbsp;</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" >&nbsp;</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" >&nbsp;</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" >&nbsp;</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" >&nbsp;</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" >&nbsp;</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" >&nbsp;</th>
656
+ <th class="blank col1" >&nbsp;</th>
657
+ <th class="blank col2" >&nbsp;</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" >&nbsp;</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" >&nbsp;</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" >&nbsp;</th>
725
+ <th class="blank col4" >&nbsp;</th>
726
+ <th class="blank col5" >&nbsp;</th>
727
+ <th class="blank col6" >&nbsp;</th>
728
+ <th class="blank col7 col_trim" >&nbsp;</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" >&nbsp;</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" >&nbsp;</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&gt;", "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&amp;", "b&amp;"]
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 "&nbsp;"
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" >&nbsp;</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"] == "&nbsp;"
1486
+ assert ctx["head"][1][1]["display_value"] == "&nbsp;"
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"] == "&nbsp;"
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"&lt;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&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;foo"),
391
+ (" foo", None, "foo"),
392
+ (
393
+ "foo foo foo",
394
+ None,
395
+ "foo foo&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; foo",
396
+ ), # odd no.of spaces
397
+ (
398
+ "foo foo foo",
399
+ None,
400
+ "foo foo&nbsp;&nbsp;&nbsp;&nbsp;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 &amp;"
174
+ b = "stri>ng2 &amp;"
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"&lt;class" not in df._repr_html_()
890
+ with option_context("display.large_repr", "info"):
891
+ assert r"&lt;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 "&lt;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)