-
-
Notifications
You must be signed in to change notification settings - Fork 19.3k
ENH: support reading directory in read_csv #61275
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
84d6bd3
16cf492
b69fad1
822dffc
5637dca
3905f1c
361c41c
02f93bd
c77158e
179f911
d7bef62
db1c7ed
91a7956
8b5cdd4
13c1258
abce2fd
70bcb2a
b99b641
14d7afc
2a445f3
3173270
f94a0bf
38bed64
2a66b92
a2b65e1
d77d290
2eee5e2
b6b48e9
f3a10e0
103db50
b7e055c
48770ec
afbfc6a
6e8aec7
f4ecd5e
8744d85
109edd8
1f310d7
f86728f
b5a0d1d
41cdb25
9d1a055
dc3ac22
3e4b032
56bb9a6
f2f6a8c
a7e04d3
3229482
e74fe09
73b8ffe
977358b
a7ebbcd
32951cc
c8f41ee
594a81b
2f0bc00
3f2e164
c756492
5e269ad
b23caa8
1dd4080
1c3f6fa
4aca1b5
91c9e6d
d85b997
4e6cde7
5600c45
6ccbc91
47e47a2
a3eebbb
4e75a1d
90a8441
63bbde8
ff6fcf1
9040aa5
57813e4
577726e
f2f70bb
3ffcb4e
daf6945
22c6579
af41080
aa7b003
1564747
19a7471
3d0a04e
0d3bf20
bb420dc
b7c9155
b07bee1
65c6f9a
9e1c9c7
e4902c7
7ed1c00
ca3f0fc
2b3d8d1
26102ab
5ab4a1e
aaa95ae
dc8ec97
6d53b13
f792a15
e7fee01
61bef41
b14c470
da1c1ed
1893382
9924ebe
90ba4c0
059d0ff
828a047
0ffeaf2
1afe0e2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |||||||||||||||||||||||||||||
| from collections import defaultdict | ||||||||||||||||||||||||||||||
| from collections.abc import ( | ||||||||||||||||||||||||||||||
| Hashable, | ||||||||||||||||||||||||||||||
| Iterable, | ||||||||||||||||||||||||||||||
| Mapping, | ||||||||||||||||||||||||||||||
| Sequence, | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
|
@@ -26,7 +27,10 @@ | |||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| import mmap | ||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||||||||||
| from pathlib import ( | ||||||||||||||||||||||||||||||
| Path, | ||||||||||||||||||||||||||||||
| PurePosixPath, | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| import re | ||||||||||||||||||||||||||||||
| import tarfile | ||||||||||||||||||||||||||||||
| from typing import ( | ||||||||||||||||||||||||||||||
|
|
@@ -42,6 +46,7 @@ | |||||||||||||||||||||||||||||
| overload, | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| from urllib.parse import ( | ||||||||||||||||||||||||||||||
| unquote, | ||||||||||||||||||||||||||||||
| urljoin, | ||||||||||||||||||||||||||||||
| urlparse as parse_url, | ||||||||||||||||||||||||||||||
| uses_netloc, | ||||||||||||||||||||||||||||||
|
|
@@ -55,6 +60,7 @@ | |||||||||||||||||||||||||||||
| BaseBuffer, | ||||||||||||||||||||||||||||||
| ReadCsvBuffer, | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| from pandas.compat import is_platform_windows | ||||||||||||||||||||||||||||||
| from pandas.compat._optional import import_optional_dependency | ||||||||||||||||||||||||||||||
| from pandas.util._decorators import doc | ||||||||||||||||||||||||||||||
| from pandas.util._exceptions import find_stack_level | ||||||||||||||||||||||||||||||
|
|
@@ -1280,3 +1286,199 @@ def dedup_names( | |||||||||||||||||||||||||||||
| counts[col] = cur_count + 1 | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return names | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| def _infer_protocol(path: str) -> str: | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
| Infer the protocol of a given path string. | ||||||||||||||||||||||||||||||
| Parameters | ||||||||||||||||||||||||||||||
| ---------- | ||||||||||||||||||||||||||||||
| path : str | ||||||||||||||||||||||||||||||
| The path string to infer the protocol from. | ||||||||||||||||||||||||||||||
| Returns | ||||||||||||||||||||||||||||||
| ------- | ||||||||||||||||||||||||||||||
| str | ||||||||||||||||||||||||||||||
| The inferred protocol. | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
| # Treat Windows drive letters like C:\ as local file paths | ||||||||||||||||||||||||||||||
| if is_platform_windows() and re.match(r"^[a-zA-Z]:[\\/]", path): | ||||||||||||||||||||||||||||||
| return "file" | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if is_fsspec_url(path) or path.startswith("http"): | ||||||||||||||||||||||||||||||
| parsed = parse_url(path) | ||||||||||||||||||||||||||||||
| return parsed.scheme | ||||||||||||||||||||||||||||||
| return "file" | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| def _match_file( | ||||||||||||||||||||||||||||||
| path: Path | PurePosixPath, extensions: set[str] | None, glob: str | None | ||||||||||||||||||||||||||||||
| ) -> bool: | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
| Check if the file matches the given extensions and glob pattern. | ||||||||||||||||||||||||||||||
| Parameters | ||||||||||||||||||||||||||||||
| ---------- | ||||||||||||||||||||||||||||||
| path : Path or PurePosixPath | ||||||||||||||||||||||||||||||
| The file path to check. | ||||||||||||||||||||||||||||||
| extensions : set[str] | ||||||||||||||||||||||||||||||
| A set of file extensions to match against. | ||||||||||||||||||||||||||||||
| glob : str | ||||||||||||||||||||||||||||||
| A glob pattern to match against. | ||||||||||||||||||||||||||||||
fangchenli marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||
| Returns | ||||||||||||||||||||||||||||||
| ------- | ||||||||||||||||||||||||||||||
| bool | ||||||||||||||||||||||||||||||
| True if the file matches the extensions and glob pattern, False otherwise. | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
| return (extensions is None or path.suffix.lower() in extensions) and ( | ||||||||||||||||||||||||||||||
| glob is None or path.match(glob) | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| def _resolve_local_path(path_str: str) -> Path: | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
| Resolve a local file path, handling Windows paths and file URLs. | ||||||||||||||||||||||||||||||
| Parameters | ||||||||||||||||||||||||||||||
| ---------- | ||||||||||||||||||||||||||||||
| path_str : str | ||||||||||||||||||||||||||||||
| The path string to resolve. | ||||||||||||||||||||||||||||||
| Returns | ||||||||||||||||||||||||||||||
| ------- | ||||||||||||||||||||||||||||||
| Path | ||||||||||||||||||||||||||||||
| A Path object representing the resolved local path. | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
| parsed = parse_url(path_str) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if is_platform_windows(): | ||||||||||||||||||||||||||||||
| if parsed.scheme == "file": | ||||||||||||||||||||||||||||||
| if parsed.netloc: | ||||||||||||||||||||||||||||||
| return Path(f"//{parsed.netloc}{unquote(parsed.path)}") | ||||||||||||||||||||||||||||||
| return Path(unquote(parsed.path.lstrip("/"))) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if re.match(r"^[a-zA-Z]:[\\/]", path_str): | ||||||||||||||||||||||||||||||
| return Path(unquote(path_str)) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return Path(unquote(parsed.path)) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| def iterdir( | ||||||||||||||||||||||||||||||
| path: FilePath | ReadCsvBuffer[bytes] | ReadCsvBuffer[str], | ||||||||||||||||||||||||||||||
| extensions: str | Iterable[str] | None = None, | ||||||||||||||||||||||||||||||
| glob: str | None = None, | ||||||||||||||||||||||||||||||
| storage_options: StorageOptions | None = None, | ||||||||||||||||||||||||||||||
| ) -> FilePath | list[FilePath] | ReadCsvBuffer[bytes] | ReadCsvBuffer[str]: | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
| Return file paths in a directory (no nesting allowed). File-like objects | ||||||||||||||||||||||||||||||
| and string URLs are returned directly. Remote paths are handled via fsspec. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Supports: | ||||||||||||||||||||||||||||||
| - Local paths (str, os.PathLike) | ||||||||||||||||||||||||||||||
| - file:// URLs | ||||||||||||||||||||||||||||||
| - Remote paths (e.g., s3://) via fsspec (if installed) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Parameters | ||||||||||||||||||||||||||||||
| ---------- | ||||||||||||||||||||||||||||||
| path : FilePath | ||||||||||||||||||||||||||||||
| Path to the directory (local or remote). | ||||||||||||||||||||||||||||||
| extensions : str or list of str, optional | ||||||||||||||||||||||||||||||
| Only return files with the given extension(s). Case-insensitive. | ||||||||||||||||||||||||||||||
| If None, all files are returned. | ||||||||||||||||||||||||||||||
| glob : str, optional | ||||||||||||||||||||||||||||||
| Only return files matching the given glob pattern. | ||||||||||||||||||||||||||||||
| If None, all files are returned. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Returns | ||||||||||||||||||||||||||||||
| ------- | ||||||||||||||||||||||||||||||
| FilePath or list[FilePath] or ReadCsvBuffer | ||||||||||||||||||||||||||||||
| If `path` is a file-like object, returns it directly. | ||||||||||||||||||||||||||||||
| Otherwise, returns a list of file paths in the directory. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Raises | ||||||||||||||||||||||||||||||
| ------ | ||||||||||||||||||||||||||||||
| TypeError | ||||||||||||||||||||||||||||||
| If `path` is not a string, os.PathLike, or file-like object. | ||||||||||||||||||||||||||||||
| FileNotFoundError | ||||||||||||||||||||||||||||||
| If the specified path does not exist. | ||||||||||||||||||||||||||||||
| ValueError | ||||||||||||||||||||||||||||||
| If the specified path is neither a file nor a directory. | ||||||||||||||||||||||||||||||
| ImportError | ||||||||||||||||||||||||||||||
| If fsspec is required but not installed. | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # file-like objects and urls are returned directly | ||||||||||||||||||||||||||||||
| if hasattr(path, "read") or hasattr(path, "write") or is_url(path): | ||||||||||||||||||||||||||||||
| return path | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if not isinstance(path, (str, os.PathLike)): | ||||||||||||||||||||||||||||||
| raise TypeError( | ||||||||||||||||||||||||||||||
| f"Expected file path name or file-like object, got {type(path)} type" | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if extensions is not None: | ||||||||||||||||||||||||||||||
| if isinstance(extensions, str): | ||||||||||||||||||||||||||||||
| extensions = {extensions.lower()} | ||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||
| extensions = {ext.lower() for ext in extensions} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| path_str = os.fspath(path) | ||||||||||||||||||||||||||||||
| scheme = _infer_protocol(path_str) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if scheme == "file": | ||||||||||||||||||||||||||||||
| resolved_path = _resolve_local_path(path_str) | ||||||||||||||||||||||||||||||
| if not resolved_path.exists(): | ||||||||||||||||||||||||||||||
| raise FileNotFoundError(f"No such file or directory: '{resolved_path}'") | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| result = [] | ||||||||||||||||||||||||||||||
| if resolved_path.is_file(): | ||||||||||||||||||||||||||||||
| if _match_file( | ||||||||||||||||||||||||||||||
| resolved_path, | ||||||||||||||||||||||||||||||
| extensions, | ||||||||||||||||||||||||||||||
| glob, | ||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||
| result.append(resolved_path) | ||||||||||||||||||||||||||||||
| return result # type: ignore[return-value] | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if resolved_path.is_dir(): | ||||||||||||||||||||||||||||||
| for entry in resolved_path.iterdir(): | ||||||||||||||||||||||||||||||
| if entry.is_file(): | ||||||||||||||||||||||||||||||
| if _match_file( | ||||||||||||||||||||||||||||||
| entry, | ||||||||||||||||||||||||||||||
| extensions, | ||||||||||||||||||||||||||||||
| glob, | ||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||
| result.append(entry) | ||||||||||||||||||||||||||||||
| return result # type: ignore[return-value] | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||||||
| f"The path '{resolved_path}' is neither a file nor a directory." | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Remote paths | ||||||||||||||||||||||||||||||
| fsspec = import_optional_dependency("fsspec") | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # GH #11071 | ||||||||||||||||||||||||||||||
| # Two legacy S3 protocols (s3n and s3a) are replaced with s3 | ||||||||||||||||||||||||||||||
| if path_str.startswith("s3n://"): | ||||||||||||||||||||||||||||||
| path_str = path_str.replace("s3n://", "s3://") | ||||||||||||||||||||||||||||||
| if path_str.startswith("s3a://"): | ||||||||||||||||||||||||||||||
| path_str = path_str.replace("s3a://", "s3://") | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| fs, inner_path = fsspec.core.url_to_fs(path_str, **(storage_options or {})) | ||||||||||||||||||||||||||||||
| if fs.isfile(inner_path): | ||||||||||||||||||||||||||||||
| path_obj = PurePosixPath(inner_path) | ||||||||||||||||||||||||||||||
| if _match_file( | ||||||||||||||||||||||||||||||
| path_obj, | ||||||||||||||||||||||||||||||
| extensions, | ||||||||||||||||||||||||||||||
| glob, | ||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||
| return [path] | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| result = [] | ||||||||||||||||||||||||||||||
| for file in fs.ls(inner_path, detail=True): | ||||||||||||||||||||||||||||||
| if file["type"] == "file": | ||||||||||||||||||||||||||||||
| path_obj = PurePosixPath(file["name"]) | ||||||||||||||||||||||||||||||
| if _match_file( | ||||||||||||||||||||||||||||||
| path_obj, | ||||||||||||||||||||||||||||||
| extensions, | ||||||||||||||||||||||||||||||
| glob, | ||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||
| result.append(f"{scheme}://{path_obj}") # type: ignore[arg-type] | ||||||||||||||||||||||||||||||
|
Comment on lines
+1477
to
+1483
|
||||||||||||||||||||||||||||||
| path_obj = PurePosixPath(file["name"]) | |
| if _match_file( | |
| path_obj, | |
| extensions, | |
| glob, | |
| ): | |
| result.append(f"{scheme}://{path_obj}") # type: ignore[arg-type] | |
| file_path_obj = PurePosixPath(file["name"]) | |
| if _match_file( | |
| file_path_obj, | |
| extensions, | |
| glob, | |
| ): | |
| result.append(f"{scheme}://{file_path_obj}") # type: ignore[arg-type] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The whatsnew entry should mention all affected functions (read_csv, read_table, and read_fwf) or use 'CSV reading functions' for accuracy, as the changes apply to multiple reader functions.