Skip to content

Commit 0d3dcbb

Browse files
committed
added capability to handle Path/LocalPath objects
1 parent 0cd6734 commit 0d3dcbb

File tree

7 files changed

+93
-6
lines changed

7 files changed

+93
-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

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ Enhancements
2323
Other Enhancements
2424
^^^^^^^^^^^^^^^^^^
2525

26+
- :func:`~pandas.io.parsers.read_csv` and :func:`~pandas.io.parsers.read_table` now
27+
also accept :class:`python:pathlib.Path`, or :class:`py:py._path.local.LocalPath`
28+
for the ``filepath_or_buffer`` argument. (:issue:`11051`)
29+
2630
.. _whatsnew_0171.api:
2731

2832
API changes

pandas/io/common.py

+38-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,24 @@
55
import zipfile
66
from contextlib import contextmanager, closing
77

8-
from pandas.compat import StringIO, string_types, BytesIO
8+
from pandas.compat import StringIO, BytesIO, string_types, text_type
99
from pandas import compat
1010

1111

12+
try:
13+
import pathlib
14+
_PATHLIB_INSTALLED = True
15+
except ImportError:
16+
_PATHLIB_INSTALLED = False
17+
18+
19+
try:
20+
from py.path import local as LocalPath
21+
_PY_PATH_INSTALLED = True
22+
except:
23+
_PY_PATH_INSTALLED = False
24+
25+
1226
if compat.PY3:
1327
from urllib.request import urlopen, pathname2url
1428
_urlopen = urlopen
@@ -201,6 +215,25 @@ def _validate_header_arg(header):
201215
"header=int or list-like of ints to specify "
202216
"the row(s) making up the column names")
203217

218+
def _stringify_path(filepath_or_buffer):
219+
"""Return the argument coerced to a string if it was a pathlib.Path
220+
or a py.path.local
221+
222+
Parameters
223+
----------
224+
filepath_or_buffer : object to be converted
225+
226+
Returns
227+
-------
228+
str_filepath_or_buffer : a the string version of the input path
229+
"""
230+
if _PATHLIB_INSTALLED and isinstance(filepath_or_buffer, pathlib.Path):
231+
return text_type(filepath_or_buffer)
232+
if _PY_PATH_INSTALLED and isinstance(filepath_or_buffer, LocalPath):
233+
return filepath_or_buffer.strpath
234+
return filepath_or_buffer
235+
236+
204237
def get_filepath_or_buffer(filepath_or_buffer, encoding=None,
205238
compression=None):
206239
"""
@@ -209,7 +242,8 @@ def get_filepath_or_buffer(filepath_or_buffer, encoding=None,
209242
210243
Parameters
211244
----------
212-
filepath_or_buffer : a url, filepath, or buffer
245+
filepath_or_buffer : a url, filepath (str, py.path.local or pathlib.Path),
246+
or buffer
213247
encoding : the encoding to use to decode py3 bytes, default is 'utf-8'
214248
215249
Returns
@@ -257,6 +291,8 @@ def get_filepath_or_buffer(filepath_or_buffer, encoding=None,
257291
filepath_or_buffer = k
258292
return filepath_or_buffer, None, compression
259293

294+
# It is a pathlib.Path/py.path.local or string
295+
filepath_or_buffer = _stringify_path(filepath_or_buffer)
260296
return _expand_user(filepath_or_buffer), None, compression
261297

262298

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)