perspective-python 5.6.0__cp311-abi3-pyemscripten_2026_0_wasm32.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. perspective/__init__.py +403 -0
  2. perspective/extension/finos-perspective-nbextension.json +5 -0
  3. perspective/handlers/__init__.py +11 -0
  4. perspective/handlers/aiohttp.py +69 -0
  5. perspective/handlers/starlette.py +63 -0
  6. perspective/handlers/tornado.py +192 -0
  7. perspective/perspective.abi3.so +0 -0
  8. perspective/templates/exported_widget.html.template +39 -0
  9. perspective/tests/__init__.py +11 -0
  10. perspective/tests/async/test_async_client.py +83 -0
  11. perspective/tests/async/test_websocket_client.py +124 -0
  12. perspective/tests/column_paths.py +33 -0
  13. perspective/tests/conftest.py +276 -0
  14. perspective/tests/core/__init__.py +11 -0
  15. perspective/tests/core/test_async.py +351 -0
  16. perspective/tests/multi_threaded/__init__.py +11 -0
  17. perspective/tests/multi_threaded/test_derived_threaded.py +103 -0
  18. perspective/tests/multi_threaded/test_multi_threaded.py +293 -0
  19. perspective/tests/server/__init__.py +11 -0
  20. perspective/tests/server/test_server.py +1016 -0
  21. perspective/tests/server/test_session.py +110 -0
  22. perspective/tests/table/__init__.py +11 -0
  23. perspective/tests/table/arrow/date32.arrow +0 -0
  24. perspective/tests/table/arrow/date64.arrow +0 -0
  25. perspective/tests/table/arrow/dict.arrow +0 -0
  26. perspective/tests/table/arrow/dict_update.arrow +0 -0
  27. perspective/tests/table/arrow/int_float_str.arrow +0 -0
  28. perspective/tests/table/arrow/int_float_str_file.arrow +0 -0
  29. perspective/tests/table/arrow/int_float_str_update.arrow +0 -0
  30. perspective/tests/table/object_sequence.py +402 -0
  31. perspective/tests/table/test_column_paths.py +88 -0
  32. perspective/tests/table/test_delete.py +124 -0
  33. perspective/tests/table/test_derived.py +145 -0
  34. perspective/tests/table/test_exception.py +73 -0
  35. perspective/tests/table/test_join.py +115 -0
  36. perspective/tests/table/test_leaks.py +105 -0
  37. perspective/tests/table/test_on_remove.py +140 -0
  38. perspective/tests/table/test_ports.py +178 -0
  39. perspective/tests/table/test_remove.py +102 -0
  40. perspective/tests/table/test_table.py +641 -0
  41. perspective/tests/table/test_table_arrow.py +500 -0
  42. perspective/tests/table/test_table_arrow_nested.py +535 -0
  43. perspective/tests/table/test_table_datetime.py +2544 -0
  44. perspective/tests/table/test_table_infer.py +201 -0
  45. perspective/tests/table/test_table_limit.py +45 -0
  46. perspective/tests/table/test_table_numpy.py +1022 -0
  47. perspective/tests/table/test_table_page_to_disk.py +304 -0
  48. perspective/tests/table/test_table_pandas.py +1018 -0
  49. perspective/tests/table/test_table_polars.py +251 -0
  50. perspective/tests/table/test_table_view_table.py +130 -0
  51. perspective/tests/table/test_to_arrow.py +416 -0
  52. perspective/tests/table/test_to_arrow_lz4.py +32 -0
  53. perspective/tests/table/test_to_arrow_zstd.py +41 -0
  54. perspective/tests/table/test_to_format.py +1030 -0
  55. perspective/tests/table/test_to_polars.py +26 -0
  56. perspective/tests/table/test_update.py +561 -0
  57. perspective/tests/table/test_update_arrow.py +980 -0
  58. perspective/tests/table/test_update_pandas.py +211 -0
  59. perspective/tests/table/test_view.py +2503 -0
  60. perspective/tests/table/test_view_expression.py +2256 -0
  61. perspective/tests/test_dependencies.py +53 -0
  62. perspective/tests/viewer/__init__.py +11 -0
  63. perspective/tests/viewer/test_viewer.py +246 -0
  64. perspective/tests/virtual_servers/__init__.py +12 -0
  65. perspective/tests/virtual_servers/test_coerce_types.py +464 -0
  66. perspective/tests/virtual_servers/test_duckdb.py +1280 -0
  67. perspective/tests/virtual_servers/test_polars.py +1075 -0
  68. perspective/tests/virtual_servers/test_postgres.py +964 -0
  69. perspective/tests/widget/__init__.py +11 -0
  70. perspective/tests/widget/test_anywidget_smoke.py +109 -0
  71. perspective/tests/widget/test_widget.py +278 -0
  72. perspective/tests/widget/test_widget_html_export.py +113 -0
  73. perspective/tests/widget/test_widget_pandas.py +453 -0
  74. perspective/virtual_servers/__init__.py +206 -0
  75. perspective/virtual_servers/clickhouse.py +296 -0
  76. perspective/virtual_servers/duckdb.py +367 -0
  77. perspective/virtual_servers/polars.py +750 -0
  78. perspective/virtual_servers/postgres.py +448 -0
  79. perspective/widget/__init__.py +351 -0
  80. perspective/widget/static/perspective-anywidget.css +1 -0
  81. perspective/widget/static/perspective-anywidget.js +31 -0
  82. perspective/widget/viewer/__init__.py +15 -0
  83. perspective/widget/viewer/viewer.py +355 -0
  84. perspective/widget/viewer/viewer_traitlets.py +98 -0
  85. perspective_python-5.6.0.dist-info/METADATA +127 -0
  86. perspective_python-5.6.0.dist-info/RECORD +90 -0
  87. perspective_python-5.6.0.dist-info/WHEEL +4 -0
  88. perspective_python-5.6.0.dist-info/licenses/LICENSE.md +193 -0
  89. perspective_python-5.6.0.dist-info/licenses/LICENSE_THIRDPARTY_cargo.yml +16075 -0
  90. perspective_python-5.6.0.dist-info/sboms/perspective-python.cyclonedx.json +5334 -0
@@ -0,0 +1,535 @@
1
+ # ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ # ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ # ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ # ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ # ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ # ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ # ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ # ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ # ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ # ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ import pyarrow as pa
14
+ import pytest
15
+ import perspective as psp
16
+
17
+
18
+ client = psp.Server().new_local_client()
19
+ Table = client.table
20
+
21
+
22
+ def arrow_bytes(table):
23
+ sink = pa.BufferOutputStream()
24
+ with pa.RecordBatchFileWriter(sink, table.schema) as writer:
25
+ writer.write_table(table)
26
+
27
+ return sink.getvalue().to_pybytes()
28
+
29
+
30
+ class TestTableArrowStruct(object):
31
+ def test_struct_flattens_to_dotted_columns(self):
32
+ data = pa.table(
33
+ {
34
+ "id": pa.array([1, 2], type=pa.int64()),
35
+ "s": pa.array(
36
+ [{"a": 10, "b": 1.5}, {"a": 20, "b": 2.5}],
37
+ type=pa.struct([("a", pa.int64()), ("b", pa.float64())]),
38
+ ),
39
+ }
40
+ )
41
+
42
+ tbl = Table(data)
43
+ assert tbl.schema() == {
44
+ "id": "integer",
45
+ "s.a": "integer",
46
+ "s.b": "float",
47
+ }
48
+
49
+ assert tbl.view().to_columns() == {
50
+ "id": [1, 2],
51
+ "s.a": [10, 20],
52
+ "s.b": [1.5, 2.5],
53
+ }
54
+
55
+ def test_struct_of_struct_recurses(self):
56
+ inner = pa.struct([("c", pa.int64())])
57
+ data = pa.table(
58
+ {
59
+ "s": pa.array(
60
+ [{"b": {"c": 1}}, {"b": {"c": 2}}],
61
+ type=pa.struct([("b", inner)]),
62
+ )
63
+ }
64
+ )
65
+
66
+ tbl = Table(data)
67
+ assert tbl.schema() == {"s.b.c": "integer"}
68
+ assert tbl.view().to_columns() == {"s.b.c": [1, 2]}
69
+
70
+ def test_struct_null_parent_nulls_all_leaves(self):
71
+ data = pa.table(
72
+ {
73
+ "s": pa.array(
74
+ [{"a": 1, "b": 2}, None, {"a": 3, "b": 4}],
75
+ type=pa.struct([("a", pa.int64()), ("b", pa.int64())]),
76
+ )
77
+ }
78
+ )
79
+
80
+ tbl = Table(data)
81
+ assert tbl.view().to_columns() == {
82
+ "s.a": [1, None, 3],
83
+ "s.b": [2, None, 4],
84
+ }
85
+
86
+ def test_struct_null_child_and_null_parent(self):
87
+ data = pa.table(
88
+ {
89
+ "s": pa.array(
90
+ [{"a": 1}, {"a": None}, None],
91
+ type=pa.struct([("a", pa.int64())]),
92
+ )
93
+ }
94
+ )
95
+
96
+ tbl = Table(data)
97
+ assert tbl.view().to_columns() == {"s.a": [1, None, None]}
98
+
99
+ def test_struct_sliced_input(self):
100
+ data = pa.table(
101
+ {
102
+ "s": pa.array(
103
+ [{"a": 1}, {"a": 2}, {"a": 3}, {"a": 4}],
104
+ type=pa.struct([("a", pa.int64())]),
105
+ )
106
+ }
107
+ ).slice(1, 2)
108
+
109
+ tbl = Table(data)
110
+ assert tbl.view().to_columns() == {"s.a": [2, 3]}
111
+
112
+ def test_struct_multi_chunk(self):
113
+ chunk = pa.array([{"a": 1}], type=pa.struct([("a", pa.int64())]))
114
+ data = pa.table({"s": pa.chunked_array([chunk, chunk, chunk])})
115
+ tbl = Table(data)
116
+ assert tbl.view().to_columns() == {"s.a": [1, 1, 1]}
117
+
118
+ def test_struct_update_matches_declared_dotted_schema(self):
119
+ tbl = Table({"id": "integer", "s.a": "integer"})
120
+ tbl.update(
121
+ arrow_bytes(
122
+ pa.table(
123
+ {
124
+ "id": pa.array([1], type=pa.int64()),
125
+ "s": pa.array(
126
+ [{"a": 7}], type=pa.struct([("a", pa.int64())])
127
+ ),
128
+ }
129
+ )
130
+ )
131
+ )
132
+
133
+ assert tbl.view().to_columns() == {"id": [1], "s.a": [7]}
134
+
135
+
136
+ class TestTableArrowList(object):
137
+ def test_list_zip_is_the_default(self):
138
+ data = pa.table(
139
+ {
140
+ "x": pa.array([1, 2], type=pa.int64()),
141
+ "y": pa.array([[10, 20, 30], [40]], type=pa.list_(pa.int64())),
142
+ }
143
+ )
144
+
145
+ tbl = Table(data)
146
+ assert tbl.schema() == {"x": "integer", "y": "integer"}
147
+ assert tbl.view().to_columns() == {
148
+ "x": [1, 1, 1, 2],
149
+ "y": [10, 20, 30, 40],
150
+ }
151
+
152
+ def test_list_empty_yields_one_null_row(self):
153
+ data = pa.table(
154
+ {
155
+ "x": pa.array([1, 2], type=pa.int64()),
156
+ "y": pa.array([[], [40]], type=pa.list_(pa.int64())),
157
+ }
158
+ )
159
+
160
+ assert Table(data).view().to_columns() == {
161
+ "x": [1, 2],
162
+ "y": [None, 40],
163
+ }
164
+
165
+ def test_list_null_yields_one_null_row(self):
166
+ data = pa.table(
167
+ {
168
+ "x": pa.array([1, 2], type=pa.int64()),
169
+ "y": pa.array([None, [40]], type=pa.list_(pa.int64())),
170
+ }
171
+ )
172
+
173
+ assert Table(data).view().to_columns() == {
174
+ "x": [1, 2],
175
+ "y": [None, 40],
176
+ }
177
+
178
+ def test_list_of_struct_composes_both_passes(self):
179
+ data = pa.table(
180
+ {
181
+ "id": pa.array([1], type=pa.int64()),
182
+ "orders": pa.array(
183
+ [[{"price": 1.5}, {"price": 2.5}]],
184
+ type=pa.list_(pa.struct([("price", pa.float64())])),
185
+ ),
186
+ }
187
+ )
188
+
189
+ tbl = Table(data)
190
+ assert tbl.schema() == {"id": "integer", "orders.price": "float"}
191
+ assert tbl.view().to_columns() == {
192
+ "id": [1, 1],
193
+ "orders.price": [1.5, 2.5],
194
+ }
195
+
196
+ def test_list_nested_inside_a_struct(self):
197
+ data = pa.table(
198
+ {
199
+ "id": pa.array([1, 2], type=pa.int64()),
200
+ "s": pa.array(
201
+ [{"a": [10, 20]}, {"a": [30]}],
202
+ type=pa.struct([("a", pa.list_(pa.int64()))]),
203
+ ),
204
+ }
205
+ )
206
+
207
+ tbl = Table(data)
208
+ assert tbl.schema() == {"id": "integer", "s.a": "integer"}
209
+ assert tbl.view().to_columns() == {
210
+ "id": [1, 1, 2],
211
+ "s.a": [10, 20, 30],
212
+ }
213
+
214
+ def test_nested_list_recurses(self):
215
+ data = pa.table(
216
+ {
217
+ "y": pa.array(
218
+ [[[1, 2], [3]]], type=pa.list_(pa.list_(pa.int64()))
219
+ )
220
+ }
221
+ )
222
+
223
+ assert Table(data).view().to_columns() == {"y": [1, 2, 3]}
224
+
225
+ def test_list_zip_equal_lengths(self):
226
+ data = pa.table(
227
+ {
228
+ "a": pa.array([[1, 2]], type=pa.list_(pa.int64())),
229
+ "b": pa.array([[3, 4]], type=pa.list_(pa.int64())),
230
+ }
231
+ )
232
+
233
+ assert Table(data).view().to_columns() == {"a": [1, 2], "b": [3, 4]}
234
+
235
+ def test_list_multi_chunk(self):
236
+ chunk = pa.array([[1, 2]], type=pa.list_(pa.int64()))
237
+ data = pa.table({"y": pa.chunked_array([chunk, chunk])})
238
+ assert Table(data).view().to_columns() == {"y": [1, 2, 1, 2]}
239
+
240
+ def test_list_cartesian(self):
241
+ data = pa.table(
242
+ {
243
+ "a": pa.array([[1, 2]], type=pa.list_(pa.int64())),
244
+ "b": pa.array([[3, 4, 5]], type=pa.list_(pa.int64())),
245
+ }
246
+ )
247
+
248
+ tbl = Table(data, list_flatten="cartesian")
249
+ assert tbl.view().to_columns() == {
250
+ "a": [1, 1, 1, 2, 2, 2],
251
+ "b": [3, 4, 5, 3, 4, 5],
252
+ }
253
+
254
+ def test_list_cartesian_empty_counts_as_one(self):
255
+ data = pa.table(
256
+ {
257
+ "a": pa.array([[1, 2]], type=pa.list_(pa.int64())),
258
+ "b": pa.array([[]], type=pa.list_(pa.int64())),
259
+ }
260
+ )
261
+
262
+ tbl = Table(data, list_flatten="cartesian")
263
+ assert tbl.view().to_columns() == {"a": [1, 2], "b": [None, None]}
264
+
265
+ def test_list_stringify_preserves_legacy_behavior(self):
266
+ data = pa.table(
267
+ {
268
+ "x": pa.array([1, 2], type=pa.int64()),
269
+ "y": pa.array([[10, 20], [30]], type=pa.list_(pa.int64())),
270
+ }
271
+ )
272
+
273
+ tbl = Table(data, list_flatten="stringify")
274
+ assert tbl.schema() == {"x": "integer", "y": "string"}
275
+ assert tbl.view().to_columns() == {
276
+ "x": [1, 2],
277
+ "y": ["[10,20]", "[30]"],
278
+ }
279
+
280
+ def test_list_stringify_integer_widths(self):
281
+ data = pa.table(
282
+ {
283
+ "i8": pa.array([[-1, 2]], type=pa.list_(pa.int8())),
284
+ "i16": pa.array([[-300, 300]], type=pa.list_(pa.int16())),
285
+ "i32": pa.array([[-70000, 70000]], type=pa.list_(pa.int32())),
286
+ "i64": pa.array(
287
+ [[-(2**40), 2**40]], type=pa.list_(pa.int64())
288
+ ),
289
+ "u8": pa.array([[255]], type=pa.list_(pa.uint8())),
290
+ "u16": pa.array([[65535]], type=pa.list_(pa.uint16())),
291
+ "u32": pa.array([[4294967295]], type=pa.list_(pa.uint32())),
292
+ # Above INT64_MAX, so a signed writer would emit a negative.
293
+ "u64": pa.array(
294
+ [[18446744073709551615]], type=pa.list_(pa.uint64())
295
+ ),
296
+ }
297
+ )
298
+
299
+ assert Table(data, list_flatten="stringify").view().to_columns() == {
300
+ "i8": ["[-1,2]"],
301
+ "i16": ["[-300,300]"],
302
+ "i32": ["[-70000,70000]"],
303
+ "i64": ["[-1099511627776,1099511627776]"],
304
+ "u8": ["[255]"],
305
+ "u16": ["[65535]"],
306
+ "u32": ["[4294967295]"],
307
+ "u64": ["[18446744073709551615]"],
308
+ }
309
+
310
+ def test_list_flatten_mode_persists_across_update(self):
311
+ schema = pa.schema(
312
+ [("x", pa.int64()), ("y", pa.list_(pa.int64()))]
313
+ )
314
+
315
+ tbl = Table(
316
+ arrow_bytes(pa.table({"x": [1], "y": [[10, 20]]}, schema=schema)),
317
+ list_flatten="cartesian",
318
+ )
319
+
320
+ tbl.update(
321
+ arrow_bytes(pa.table({"x": [2], "y": [[30, 40]]}, schema=schema))
322
+ )
323
+
324
+ assert tbl.view().to_columns() == {
325
+ "x": [1, 1, 2, 2],
326
+ "y": [10, 20, 30, 40],
327
+ }
328
+
329
+ def test_list_limit_counts_expanded_rows(self):
330
+ data = pa.table(
331
+ {"y": pa.array([[1, 2, 3, 4]], type=pa.list_(pa.int64()))}
332
+ )
333
+
334
+ assert Table(data, limit=2).view().to_columns() == {"y": [3, 4]}
335
+
336
+
337
+ class TestTableArrowExpandedIndex(object):
338
+ """An index is only rejected when expansion would REPEAT it. A column drawn
339
+ from the exploded list takes a distinct element per row, so it is a
340
+ legitimate key."""
341
+
342
+ def orders(self, ids, prices):
343
+ return pa.table(
344
+ {
345
+ "batch": pa.array([1], type=pa.int64()),
346
+ "orders": pa.array(
347
+ [[{"id": i, "price": p} for i, p in zip(ids, prices)]],
348
+ type=pa.list_(
349
+ pa.struct([("id", pa.int64()), ("price", pa.float64())])
350
+ ),
351
+ ),
352
+ }
353
+ )
354
+
355
+ def test_index_on_a_column_from_the_list_is_allowed(self):
356
+ tbl = Table(self.orders([1, 2, 3], [1.5, 2.5, 3.5]), index="orders.id")
357
+ assert tbl.view().to_columns() == {
358
+ "batch": [1, 1, 1],
359
+ "orders.id": [1, 2, 3],
360
+ "orders.price": [1.5, 2.5, 3.5],
361
+ }
362
+
363
+ def test_index_from_the_list_updates_by_element(self):
364
+ tbl = Table(self.orders([1, 2], [1.5, 2.5]), index="orders.id")
365
+ tbl.update(arrow_bytes(self.orders([2, 3], [9.5, 3.5])))
366
+ assert tbl.view().to_columns() == {
367
+ "batch": [1, 1, 1],
368
+ "orders.id": [1, 2, 3],
369
+ "orders.price": [1.5, 9.5, 3.5],
370
+ }
371
+
372
+ def test_index_on_a_sibling_is_rejected(self):
373
+ with pytest.raises(psp.PerspectiveError, match=r"`batch`"):
374
+ Table(self.orders([1, 2], [1.5, 2.5]), index="batch")
375
+
376
+ def test_index_on_a_list_column_itself_is_allowed(self):
377
+ data = pa.table(
378
+ {
379
+ "x": pa.array([1], type=pa.int64()),
380
+ "id": pa.array([[10, 20]], type=pa.list_(pa.int64())),
381
+ }
382
+ )
383
+
384
+ assert Table(data, index="id").view().to_columns() == {
385
+ "x": [1, 1],
386
+ "id": [10, 20],
387
+ }
388
+
389
+ def test_cartesian_with_two_lists_repeats_every_column(self):
390
+ data = pa.table(
391
+ {
392
+ "id": pa.array([[1, 2]], type=pa.list_(pa.int64())),
393
+ "b": pa.array([[3, 4, 5]], type=pa.list_(pa.int64())),
394
+ }
395
+ )
396
+
397
+ with pytest.raises(psp.PerspectiveError, match=r"`id`"):
398
+ Table(data, index="id", list_flatten="cartesian")
399
+
400
+ def test_cartesian_with_one_list_is_allowed(self):
401
+ data = pa.table(
402
+ {
403
+ "x": pa.array([1], type=pa.int64()),
404
+ "id": pa.array([[10, 20]], type=pa.list_(pa.int64())),
405
+ }
406
+ )
407
+
408
+ tbl = Table(data, index="id", list_flatten="cartesian")
409
+ assert tbl.view().to_columns() == {"x": [1, 1], "id": [10, 20]}
410
+
411
+ def test_stringify_keeps_a_sibling_index_usable(self):
412
+ data = pa.table(
413
+ {
414
+ "id": pa.array([1, 2], type=pa.int64()),
415
+ "y": pa.array([[10, 20], [30]], type=pa.list_(pa.int64())),
416
+ }
417
+ )
418
+
419
+ tbl = Table(data, index="id", list_flatten="stringify")
420
+ assert tbl.view().to_columns() == {
421
+ "id": [1, 2],
422
+ "y": ["[10,20]", "[30]"],
423
+ }
424
+
425
+
426
+ class TestTableArrowGather(object):
427
+ """The expansion is deferred into the `t_column` write rather than
428
+ materialized in Arrow, so these stress the gather paths specifically."""
429
+
430
+ def test_expansion_gathers_across_chunks(self):
431
+ x = pa.chunked_array(
432
+ [pa.array([1], type=pa.int64()), pa.array([2], type=pa.int64())]
433
+ )
434
+
435
+ y = pa.chunked_array(
436
+ [
437
+ pa.array([[10, 20]], type=pa.list_(pa.int64())),
438
+ pa.array([[30]], type=pa.list_(pa.int64())),
439
+ ]
440
+ )
441
+
442
+ assert Table(pa.table({"x": x, "y": y})).view().to_columns() == {
443
+ "x": [1, 1, 2],
444
+ "y": [10, 20, 30],
445
+ }
446
+
447
+ def test_expansion_gathers_strings(self):
448
+ data = pa.table(
449
+ {
450
+ "s": pa.array(["a", "b"], type=pa.string()),
451
+ "y": pa.array([[1, 2, 3], [4]], type=pa.list_(pa.int64())),
452
+ }
453
+ )
454
+
455
+ assert Table(data).view().to_columns() == {
456
+ "s": ["a", "a", "a", "b"],
457
+ "y": [1, 2, 3, 4],
458
+ }
459
+
460
+ def test_expansion_gathers_nulls_in_siblings(self):
461
+ data = pa.table(
462
+ {
463
+ "x": pa.array([None, 2], type=pa.int64()),
464
+ "y": pa.array([[10, 20], [30]], type=pa.list_(pa.int64())),
465
+ }
466
+ )
467
+
468
+ assert Table(data).view().to_columns() == {
469
+ "x": [None, None, 2],
470
+ "y": [10, 20, 30],
471
+ }
472
+
473
+ def test_expansion_of_all_empty_lists(self):
474
+ data = pa.table(
475
+ {
476
+ "x": pa.array([1, 2], type=pa.int64()),
477
+ "y": pa.array([[], []], type=pa.list_(pa.int64())),
478
+ }
479
+ )
480
+
481
+ assert Table(data).view().to_columns() == {
482
+ "x": [1, 2],
483
+ "y": [None, None],
484
+ }
485
+
486
+ def test_sliced_input_with_nulls(self):
487
+ data = pa.table(
488
+ {"a": pa.array([1, None, 3, None, 5], type=pa.int64())}
489
+ ).slice(1, 3)
490
+
491
+ assert Table(data).view().to_columns() == {"a": [None, 3, None]}
492
+
493
+
494
+ class TestTableArrowFlatUnaffected(object):
495
+ """A table with neither struct nor list columns must be untouched by
496
+ normalization; these lock the flat path against regressions."""
497
+
498
+ def test_flat_arrow_roundtrip(self):
499
+ data = pa.table(
500
+ {
501
+ "a": pa.array([1, 2, 3], type=pa.int64()),
502
+ "b": pa.array(["x", "y", "z"], type=pa.string()),
503
+ "c": pa.array([1.5, 2.5, 3.5], type=pa.float64()),
504
+ }
505
+ )
506
+
507
+ tbl = Table(data)
508
+ assert tbl.schema() == {"a": "integer", "b": "string", "c": "float"}
509
+ assert tbl.view().to_columns() == {
510
+ "a": [1, 2, 3],
511
+ "b": ["x", "y", "z"],
512
+ "c": [1.5, 2.5, 3.5],
513
+ }
514
+
515
+ def test_flat_arrow_indexed_update(self):
516
+ data = pa.table(
517
+ {
518
+ "a": pa.array([1, 2], type=pa.int64()),
519
+ "b": pa.array([10, 20], type=pa.int64()),
520
+ }
521
+ )
522
+
523
+ tbl = Table(data, index="a")
524
+ tbl.update(
525
+ arrow_bytes(
526
+ pa.table(
527
+ {
528
+ "a": pa.array([2], type=pa.int64()),
529
+ "b": pa.array([99], type=pa.int64()),
530
+ }
531
+ )
532
+ )
533
+ )
534
+
535
+ assert tbl.view().to_columns() == {"a": [1, 2], "b": [10, 99]}