Skip to content

Commit 82f0033

Browse files
flying-sheepjreback
authored andcommitted
ENH: added capability to handle Path/LocalPath objects, #11033
1 parent 472e6e0 commit 82f0033

File tree

7 files changed

+92
-6
lines changed

7 files changed

+92
-6
lines changed

ci/requirements-2.7.pip

+2
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ blosc
22
httplib2
33
google-api-python-client == 1.2
44
python-gflags == 2.0
5+
pathlib
6+
py

doc/source/conf.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -299,8 +299,9 @@
299299
intersphinx_mapping = {
300300
'statsmodels': ('http://statsmodels.sourceforge.net/devel/', None),
301301
'matplotlib': ('http://matplotlib.org/', None),
302-
'python': ('http://docs.python.org/', None),
303-
'numpy': ('http://docs.scipy.org/doc/numpy', None)
302+
'python': ('http://docs.python.org/3', None),
303+
'numpy': ('http://docs.scipy.org/doc/numpy', None),
304+
'py': ('http://pylib.readthedocs.org/en/latest/', None)
304305
}
305306
import glob
306307
autosummary_generate = glob.glob("*.rst")

doc/source/io.rst

+3-2
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,10 @@ for some advanced strategies
7979

8080
They can take a number of arguments:
8181

82-
- ``filepath_or_buffer``: Either a string path to a file, URL
82+
- ``filepath_or_buffer``: Either a path to a file (a :class:`python:str`,
83+
:class:`python:pathlib.Path`, or :class:`py:py._path.local.LocalPath`), URL
8384
(including http, ftp, and S3 locations), or any object with a ``read``
84-
method (such as an open file or ``StringIO``).
85+
method (such as an open file or :class:`~python:io.StringIO`).
8586
- ``sep`` or ``delimiter``: A delimiter / separator to split fields
8687
on. With ``sep=None``, ``read_csv`` will try to infer the delimiter
8788
automatically in some cases by "sniffing".

doc/source/whatsnew/v0.17.1.txt

+3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ Enhancements
2828
Other Enhancements
2929
^^^^^^^^^^^^^^^^^^
3030

31+
- ``pd.read_*`` functions can now accept also accept :class:`python:pathlib.Path`, or :class:`py:py._path.local.LocalPath`
32+
objects for the ``filepath_or_buffer`` argument. (:issue:`11033`)
33+
3134
.. _whatsnew_0171.api:
3235

3336
API changes

pandas/io/common.py

+38-2
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,25 @@
77
import zipfile
88
from contextlib import contextmanager, closing
99

10-
from pandas.compat import StringIO, string_types, BytesIO
10+
from pandas.compat import StringIO, BytesIO, string_types, text_type
1111
from pandas import compat
1212
from pandas.core.common import pprint_thing, is_number
1313

1414

15+
try:
16+
import pathlib
17+
_PATHLIB_INSTALLED = True
18+
except ImportError:
19+
_PATHLIB_INSTALLED = False
20+
21+
22+
try:
23+
from py.path import local as LocalPath
24+
_PY_PATH_INSTALLED = True
25+
except:
26+
_PY_PATH_INSTALLED = False
27+
28+
1529
if compat.PY3:
1630
from urllib.request import urlopen, pathname2url
1731
_urlopen = urlopen
@@ -204,6 +218,25 @@ def _validate_header_arg(header):
204218
"header=int or list-like of ints to specify "
205219
"the row(s) making up the column names")
206220

221+
def _stringify_path(filepath_or_buffer):
222+
"""Return the argument coerced to a string if it was a pathlib.Path
223+
or a py.path.local
224+
225+
Parameters
226+
----------
227+
filepath_or_buffer : object to be converted
228+
229+
Returns
230+
-------
231+
str_filepath_or_buffer : a the string version of the input path
232+
"""
233+
if _PATHLIB_INSTALLED and isinstance(filepath_or_buffer, pathlib.Path):
234+
return text_type(filepath_or_buffer)
235+
if _PY_PATH_INSTALLED and isinstance(filepath_or_buffer, LocalPath):
236+
return filepath_or_buffer.strpath
237+
return filepath_or_buffer
238+
239+
207240
def get_filepath_or_buffer(filepath_or_buffer, encoding=None,
208241
compression=None):
209242
"""
@@ -212,7 +245,8 @@ def get_filepath_or_buffer(filepath_or_buffer, encoding=None,
212245
213246
Parameters
214247
----------
215-
filepath_or_buffer : a url, filepath, or buffer
248+
filepath_or_buffer : a url, filepath (str, py.path.local or pathlib.Path),
249+
or buffer
216250
encoding : the encoding to use to decode py3 bytes, default is 'utf-8'
217251
218252
Returns
@@ -260,6 +294,8 @@ def get_filepath_or_buffer(filepath_or_buffer, encoding=None,
260294
filepath_or_buffer = k
261295
return filepath_or_buffer, None, compression
262296

297+
# It is a pathlib.Path/py.path.local or string
298+
filepath_or_buffer = _stringify_path(filepath_or_buffer)
263299
return _expand_user(filepath_or_buffer), None, compression
264300

265301

pandas/io/tests/test_common.py

+26
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,20 @@
55
import os
66
from os.path import isabs
77

8+
import nose
89
import pandas.util.testing as tm
910

1011
from pandas.io import common
1112

13+
try:
14+
from pathlib import Path
15+
except ImportError:
16+
pass
17+
18+
try:
19+
from py.path import local as LocalPath
20+
except ImportError:
21+
pass
1222

1323
class TestCommonIOCapabilities(tm.TestCase):
1424

@@ -27,6 +37,22 @@ def test_expand_user_normal_path(self):
2737
self.assertEqual(expanded_name, filename)
2838
self.assertEqual(os.path.expanduser(filename), expanded_name)
2939

40+
def test_stringify_path_pathlib(self):
41+
tm._skip_if_no_pathlib()
42+
43+
rel_path = common._stringify_path(Path('.'))
44+
self.assertEqual(rel_path, '.')
45+
redundant_path = common._stringify_path(Path('foo//bar'))
46+
self.assertEqual(redundant_path, 'foo/bar')
47+
48+
def test_stringify_path_localpath(self):
49+
tm._skip_if_no_localpath()
50+
51+
path = 'foo/bar'
52+
abs_path = os.path.abspath(path)
53+
lpath = LocalPath(path)
54+
self.assertEqual(common._stringify_path(lpath), abs_path)
55+
3056
def test_get_filepath_or_buffer_with_path(self):
3157
filename = '~/sometest'
3258
filepath_or_buffer, _, _ = common.get_filepath_or_buffer(filename)

pandas/util/testing.py

+17
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,23 @@ def _skip_if_python26():
255255
import nose
256256
raise nose.SkipTest("skipping on python2.6")
257257

258+
259+
def _skip_if_no_pathlib():
260+
try:
261+
from pathlib import Path
262+
except ImportError:
263+
import nose
264+
raise nose.SkipTest("pathlib not available")
265+
266+
267+
def _skip_if_no_localpath():
268+
try:
269+
from py.path import local as LocalPath
270+
except ImportError:
271+
import nose
272+
raise nose.SkipTest("py.path not installed")
273+
274+
258275
def _incompat_bottleneck_version(method):
259276
""" skip if we have bottleneck installed
260277
and its >= 1.0

0 commit comments

Comments
 (0)