1from __future__ import annotations
2
3from collections import defaultdict
4from typing import TYPE_CHECKING
5import warnings
6
7import numpy as np
8
9from pandas._libs import (
10 lib,
11 parsers,
12)
13from pandas.compat._optional import import_optional_dependency
14from pandas.errors import DtypeWarning
15from pandas.util._exceptions import find_stack_level
16
17from pandas.core.dtypes.common import pandas_dtype
18from pandas.core.dtypes.concat import (
19 concat_compat,
20 union_categoricals,
21)
22from pandas.core.dtypes.dtypes import CategoricalDtype
23
24from pandas.core.indexes.api import ensure_index_from_sequences
25
26from pandas.io.common import (
27 dedup_names,
28 is_potential_multi_index,
29)
30from pandas.io.parsers.base_parser import (
31 ParserBase,
32 ParserError,
33 date_converter,
34 evaluate_callable_usecols,
35 is_index_col,
36 validate_parse_dates_presence,
37)
38
39if TYPE_CHECKING:
40 from collections.abc import (
41 Hashable,
42 Mapping,
43 Sequence,
44 )
45
46 from pandas._typing import (
47 AnyArrayLike,
48 ArrayLike,
49 DtypeArg,
50 DtypeObj,
51 ReadCsvBuffer,
52 SequenceT,
53 )
54
55 from pandas import (
56 Index,
57 MultiIndex,
58 )
59
60
61class CParserWrapper(ParserBase):
62 low_memory: bool
63 _reader: parsers.TextReader
64
65 def __init__(self, src: ReadCsvBuffer[str], **kwds) -> None:
66 super().__init__(kwds)
67 self.kwds = kwds
68 kwds = kwds.copy()
69
70 self.low_memory = kwds.pop("low_memory", False)
71
72 # #2442
73 kwds["allow_leading_cols"] = self.index_col is not False
74
75 # GH20529, validate usecol arg before TextReader
76 kwds["usecols"] = self.usecols
77
78 # Have to pass int, would break tests using TextReader directly otherwise :(
79 kwds["on_bad_lines"] = self.on_bad_lines.value
80
81 for key in (
82 "storage_options",
83 "encoding",
84 "memory_map",
85 "compression",
86 ):
87 kwds.pop(key, None)
88
89 kwds["dtype"] = ensure_dtype_objs(kwds.get("dtype", None))
90 if "dtype_backend" not in kwds or kwds["dtype_backend"] is lib.no_default:
91 kwds["dtype_backend"] = "numpy"
92 if kwds["dtype_backend"] == "pyarrow":
93 # Fail here loudly instead of in cython after reading
94 import_optional_dependency("pyarrow")
95 self._reader = parsers.TextReader(src, **kwds)
96
97 self.unnamed_cols = self._reader.unnamed_cols
98
99 passed_names = self.names is None
100
101 if self._reader.header is None:
102 self.names = None
103 else:
104 (
105 self.names,
106 self.index_names,
107 self.col_names,
108 passed_names,
109 ) = self._extract_multi_indexer_columns(
110 self._reader.header,
111 self.index_names,
112 passed_names,
113 )
114
115 if self.names is None:
116 self.names = list(range(self._reader.table_width))
117
118 # gh-9755
119 #
120 # need to set orig_names here first
121 # so that proper indexing can be done
122 # with _set_noconvert_columns
123 #
124 # once names has been filtered, we will
125 # then set orig_names again to names
126 self.orig_names = self.names[:]
127
128 if self.usecols:
129 usecols = evaluate_callable_usecols(self.usecols, self.orig_names)
130
131 # GH 14671
132 # assert for mypy, orig_names is List or None, None would error in issubset
133 assert self.orig_names is not None
134 if self.usecols_dtype == "string" and not set(usecols).issubset(
135 self.orig_names
136 ):
137 self._validate_usecols_names(usecols, self.orig_names)
138
139 if len(self.names) > len(usecols):
140 self.names = [
141 n
142 for i, n in enumerate(self.names)
143 if (i in usecols or n in usecols)
144 ]
145
146 if len(self.names) < len(usecols):
147 self._validate_usecols_names(
148 usecols,
149 self.names,
150 )
151
152 validate_parse_dates_presence(self.parse_dates, self.names)
153 self._set_noconvert_columns()
154
155 self.orig_names = self.names
156
157 if self._reader.leading_cols == 0 and is_index_col(self.index_col):
158 (
159 index_names,
160 self.names,
161 self.index_col,
162 ) = self._clean_index_names(
163 self.names,
164 self.index_col,
165 )
166
167 if self.index_names is None:
168 self.index_names = index_names
169
170 if self._reader.header is None and not passed_names:
171 assert self.index_names is not None
172 self.index_names = [None] * len(self.index_names)
173
174 self._implicit_index = self._reader.leading_cols > 0
175
176 def close(self) -> None:
177 # close handles opened by C parser
178 try:
179 self._reader.close()
180 except ValueError:
181 pass
182
183 def _set_noconvert_columns(self) -> None:
184 """
185 Set the columns that should not undergo dtype conversions.
186
187 Currently, any column that is involved with date parsing will not
188 undergo such conversions.
189 """
190 assert self.orig_names is not None
191 # error: Cannot determine type of 'names'
192
193 # much faster than using orig_names.index(x) xref GH#44106
194 names_dict = {x: i for i, x in enumerate(self.orig_names)}
195 col_indices = [names_dict[x] for x in self.names]
196 noconvert_columns = self._set_noconvert_dtype_columns(
197 col_indices,
198 self.names,
199 )
200 for col in noconvert_columns:
201 self._reader.set_noconvert(col)
202
203 def _low_memory_column_labels(
204 self, chunks: list[dict[int, ArrayLike]]
205 ) -> dict[int, Hashable]:
206 """
207 Map the field positions ``read_low_memory`` keys its chunks by to labels.
208
209 The positions are those of the source row, so they skip whatever
210 ``usecols`` dropped and start past a leading implicit index column.
211 ``orig_names`` holds exactly the named columns that remain, in the same
212 order, which is how ``read`` renames these keys further down. A leading
213 implicit index column has no name of its own and maps to ``None``.
214 """
215 assert self.orig_names is not None
216 positions = sorted(chunks[0])
217 named = dict(
218 zip(
219 positions[self._reader.leading_cols :],
220 self.orig_names,
221 strict=False,
222 )
223 )
224 return {position: named.get(position) for position in positions}
225
226 def read(
227 self,
228 nrows: int | None = None,
229 ) -> tuple[
230 Index | MultiIndex | None,
231 Sequence[Hashable] | MultiIndex,
232 Mapping[Hashable, AnyArrayLike],
233 ]:
234 index: Index | MultiIndex | None
235 column_names: Sequence[Hashable] | MultiIndex
236 try:
237 if self.low_memory:
238 chunks = self._reader.read_low_memory(nrows)
239 # destructive to chunks
240 data = _concatenate_chunks(
241 chunks, self._low_memory_column_labels(chunks)
242 )
243 else:
244 data = self._reader.read(nrows)
245 except StopIteration:
246 if self._first_chunk:
247 self._first_chunk = False
248 # assert for mypy, orig_names is List or None, None would error in
249 # list(...) in dedup_names
250 assert self.orig_names is not None
251 names = dedup_names(
252 self.orig_names,
253 is_potential_multi_index(self.orig_names, self.index_col),
254 )
255 index, columns, col_dict = self._get_empty_meta(
256 names,
257 dtype=self.dtype,
258 )
259 # error: Incompatible types in assignment (expression has type
260 # "list[Hashable] | MultiIndex", variable has type "list[Hashable]")
261 columns = self._maybe_make_multi_index_columns( # type: ignore[assignment]
262 columns, self.col_names
263 )
264
265 columns = _filter_usecols(self.usecols, columns)
266 columns_set = set(columns)
267
268 col_dict = {k: v for k, v in col_dict.items() if k in columns_set}
269
270 return index, columns, col_dict
271
272 else:
273 self.close()
274 raise
275
276 # Done with first read, next time raise StopIteration
277 self._first_chunk = False
278
279 names = self.names
280
281 if self._reader.leading_cols:
282 # implicit index, no index names
283 arrays = []
284
285 if self.index_col and self._reader.leading_cols != len(self.index_col):
286 raise ParserError(
287 "Could not construct index. Requested to use "
288 f"{len(self.index_col)} number of columns, but "
289 f"{self._reader.leading_cols} left to parse."
290 )
291
292 for i in range(self._reader.leading_cols):
293 if self.index_col is None:
294 values = data.pop(i)
295 else:
296 values = data.pop(self.index_col[i])
297
298 if self._should_parse_dates(i):
299 values = date_converter(
300 values,
301 col=(
302 self.index_names[i]
303 if self.index_names is not None
304 else None
305 ),
306 dayfirst=self.dayfirst,
307 cache_dates=self.cache_dates,
308 date_format=self.date_format,
309 )
310 arrays.append(values)
311
312 index = ensure_index_from_sequences(arrays)
313
314 names = _filter_usecols(self.usecols, names)
315
316 names = dedup_names(names, is_potential_multi_index(names, self.index_col))
317
318 # rename dict keys
319 data_tups = sorted(data.items())
320 data = {k: v for k, (i, v) in zip(names, data_tups, strict=True)}
321
322 date_data = self._do_date_conversions(names, data)
323
324 # maybe create a mi on the columns
325 column_names = self._maybe_make_multi_index_columns(names, self.col_names)
326
327 else:
328 # rename dict keys
329 data_tups = sorted(data.items())
330
331 # ugh, mutation
332
333 # assert for mypy, orig_names is List or None, None would error in list(...)
334 assert self.orig_names is not None
335 names = list(self.orig_names)
336 names = dedup_names(names, is_potential_multi_index(names, self.index_col))
337
338 names = _filter_usecols(self.usecols, names)
339
340 # columns as list
341 alldata = [x[1] for x in data_tups]
342 if self.usecols is None:
343 self._check_data_length(names, alldata)
344
345 data = {k: v for k, (i, v) in zip(names, data_tups, strict=False)}
346
347 date_data = self._do_date_conversions(names, data)
348 index, column_names = self._make_index(alldata, names)
349
350 return index, column_names, date_data
351
352
353def _filter_usecols(usecols, names: SequenceT) -> SequenceT | list[Hashable]:
354 # hackish
355 usecols = evaluate_callable_usecols(usecols, names)
356 if usecols is not None and len(names) != len(usecols):
357 return [name for i, name in enumerate(names) if i in usecols or name in usecols]
358 return names
359
360
361def _concatenate_chunks(
362 chunks: list[dict[int, ArrayLike]],
363 column_names: Sequence[Hashable] | Mapping[int, Hashable],
364) -> dict:
365 """
366 Concatenate chunks of data read with low_memory=True.
367
368 The tricky part is handling Categoricals, where different chunks
369 may have different inferred categories.
370
371 ``column_names`` names the keys of ``chunks`` for the mixed-dtype warning,
372 so it has to be indexable by whatever those keys are: the low_memory reader
373 keys its chunks by field position and passes a mapping keyed the same way,
374 while the parallel reader keys its own by name.
375 """
376 names = list(chunks[0].keys())
377 warning_columns = []
378
379 result: dict = {}
380 for name in names:
381 arrs = [chunk.pop(name) for chunk in chunks]
382 # Check each arr for consistent types.
383 dtypes = {a.dtype for a in arrs}
384 non_cat_dtypes = {x for x in dtypes if not isinstance(x, CategoricalDtype)}
385
386 dtype = dtypes.pop()
387 if isinstance(dtype, CategoricalDtype):
388 result[name] = union_categoricals(arrs, sort_categories=False)
389 else:
390 result[name] = concat_compat(arrs)
391 if len(non_cat_dtypes) > 1 and result[name].dtype == np.dtype(object):
392 warning_columns.append((name, column_names[name]))
393
394 if warning_columns:
395 warning_names = ", ".join(
396 [
397 f"{position}: {label}" if label is not None else f"{position}"
398 for position, label in warning_columns
399 ]
400 )
401 warning_message = " ".join(
402 [
403 f"Columns ({warning_names}) have mixed types. "
404 f"Specify dtype option on import or set low_memory=False."
405 ]
406 )
407 warnings.warn(warning_message, DtypeWarning, stacklevel=find_stack_level())
408 return result
409
410
411def ensure_dtype_objs(
412 dtype: DtypeArg | dict[Hashable, DtypeArg] | None,
413) -> DtypeObj | dict[Hashable, DtypeObj] | None:
414 """
415 Ensure we have either None, a dtype object, or a dictionary mapping to
416 dtype objects.
417 """
418 if isinstance(dtype, defaultdict):
419 # "None" not callable [misc]
420 default_dtype = pandas_dtype(dtype.default_factory()) # type: ignore[misc]
421 dtype_converted: defaultdict = defaultdict(lambda: default_dtype)
422 for key in dtype.keys():
423 dtype_converted[key] = pandas_dtype(dtype[key])
424 return dtype_converted
425 elif isinstance(dtype, dict):
426 return {k: pandas_dtype(dtype[k]) for k in dtype}
427 elif dtype is not None:
428 return pandas_dtype(dtype)
429 return dtype