internlm/SWE-Fixer-Train-110K huggingface dataset:
Repository: dask/dask

Issue:
BUG: dask.array.maximum can trigger eager evaluation
The failure case seems to be `maximum(scalar, dask_array)`:
```
In [1]: import dask.array as da

In [2]: x = da.ones((3,), chunks=(3,))

In [3]: da.maximum(0, x)
Out[3]: array([ 1.,  1.,  1.])

In [4]: da.maximum(x, 0)
Out[4]: dask.array<maximum..., shape=(3,), dtype=float64, chunksize=(3,)>

In [5]: 0 + x
Out[5]: dask.array<add-5f9..., shape=(3,), dtype=float64, chunksize=(3,)>
```

Pretty sure this was introduced by https://github.com/dask/dask/pull/1669

This is causing the xarray test suite to fail: https://github.com/pydata/xarray/issues/1090

CC @sinhrks


Discussion:

Resolving patch:
diff --git a/dask/array/ufunc.py b/dask/array/ufunc.py
--- a/dask/array/ufunc.py
+++ b/dask/array/ufunc.py
@@ -17,15 +17,16 @@ def __array_wrap__(numpy_ufunc, x, *args, **kwargs):
 def wrap_elemwise(numpy_ufunc, array_wrap=False):
     """ Wrap up numpy function into dask.array """
 
-    def wrapped(x, *args, **kwargs):
-        if hasattr(x, '_elemwise'):
+    def wrapped(*args, **kwargs):
+        dsk = [arg for arg in args if hasattr(arg, '_elemwise')]
+        if len(dsk) > 0:
             if array_wrap:
-                return x._elemwise(__array_wrap__, numpy_ufunc,
-                                   x, *args, **kwargs)
+                return dsk[0]._elemwise(__array_wrap__, numpy_ufunc,
+                                        *args, **kwargs)
             else:
-                return x._elemwise(numpy_ufunc, x, *args, **kwargs)
+                return dsk[0]._elemwise(numpy_ufunc, *args, **kwargs)
         else:
-            return numpy_ufunc(x, *args, **kwargs)
+            return numpy_ufunc(*args, **kwargs)
 
     # functools.wraps cannot wrap ufunc in Python 2.x
     wrapped.__name__ = numpy_ufunc.__name__


Test patch:
diff --git a/dask/array/tests/test_ufunc.py b/dask/array/tests/test_ufunc.py
--- a/dask/array/tests/test_ufunc.py
+++ b/dask/array/tests/test_ufunc.py
@@ -75,6 +75,19 @@ def test_ufunc_2args(ufunc):
     assert isinstance(dafunc(arr1, arr2), np.ndarray)
     assert_eq(dafunc(arr1, arr2), npfunc(arr1, arr2))
 
+    # with scalar
+    assert isinstance(dafunc(darr1, 10), da.Array)
+    assert_eq(dafunc(darr1, 10), npfunc(arr1, 10))
+
+    assert isinstance(dafunc(10, darr1), da.Array)
+    assert_eq(dafunc(10, darr1), npfunc(10, arr1))
+
+    assert isinstance(dafunc(arr1, 10), np.ndarray)
+    assert_eq(dafunc(arr1, 10), npfunc(arr1, 10))
+
+    assert isinstance(dafunc(10, arr1), np.ndarray)
+    assert_eq(dafunc(10, arr1), npfunc(10, arr1))
+
 
 @pytest.mark.parametrize('ufunc', ['isreal', 'iscomplex', 'real', 'imag'])
 def test_complex(ufunc):
Repository: dask/dask

Issue:
dask 0.14.0  Sharedict can't copy in threaded get_async
Hi, 

Here's the snippet to trigger the bug in question.
```python
In [1]: import dask.array as da

In [2]: import dask

In [3]: ar = da.ones((3,2), chunks=2)

In [4]: dask.threaded.get(ar.dask, (ar.name, 0,0))
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-4-261f16b94a0e> in <module>()
----> 1 dask.threaded.get(ar.dask, (ar.name, 0,0))

/home/to266/anaconda3/lib/python3.6/site-packages/dask/threaded.py in get(dsk, result, cache, num_workers, **kwargs)
     74     results = get_async(pool.apply_async, len(pool._pool), dsk, result,
     75                         cache=cache, get_id=_thread_get_id,
---> 76                         **kwargs)
     77 
     78     # Cleanup pools associated to dead threads

/home/to266/anaconda3/lib/python3.6/site-packages/dask/async.py in get_async(apply_async, num_workers, dsk, result, cache, get_id, raise_on_exception, rerun_exceptions_locally, callbacks, dumps, loads, **kwargs)
    442     results = set(result_flat)
    443 
--> 444     dsk = dsk.copy()
    445     started_cbs = []
    446     try:

AttributeError: 'ShareDict' object has no attribute 'copy'

In [5]: dask.multiprocessing.get(ar.dask, (ar.name, 0,0))
Out[5]: 
array([[ 1.,  1.],
       [ 1.,  1.]])

```
The bug is not present in `dask.multiprocessing.get`, and I did not test on distributed.


Discussion:

Resolving patch:
diff --git a/dask/async.py b/dask/async.py
--- a/dask/async.py
+++ b/dask/async.py
@@ -441,7 +441,7 @@ def get_async(apply_async, num_workers, dsk, result, cache=None,
         result_flat = set([result])
     results = set(result_flat)
 
-    dsk = dsk.copy()
+    dsk = dict(dsk)
     started_cbs = []
     try:
         for cb in callbacks:


Test patch:
diff --git a/dask/utils_test.py b/dask/utils_test.py
--- a/dask/utils_test.py
+++ b/dask/utils_test.py
@@ -125,3 +125,13 @@ def test_get_stack_limit(self):
             assert False, msg
 
         assert self.get(d, 'x4999') == 4999
+
+    def test_with_sharedict(self):
+        from .sharedict import ShareDict
+
+        dsk = ShareDict()
+        dsk.update({'x': 1,
+                    'y': (inc, 'x')})
+        dsk.update({'z': (add, (inc, 'x'), 'y')})
+
+        assert self.get(dsk, 'z') == 4
Repository: dask/dask

Issue:
Read_parquet with custom columns incorrect
If the parameter columns are not alphabetically sorted, the _meta.columns and actual DataFrame.columns differ. I noticed this when renaming columns, it mixed them up.
```
def test_read_parquet_custom_columns():
    with tmpdir() as tmp:
        tmp = str(tmp)
        data = pd.DataFrame({'i32': np.arange(1000, dtype=np.int32),
                             'f': np.arange(1000, dtype=np.float64)})
        df = dd.from_pandas(data, chunksize=50)
        df.to_parquet(tmp, write_index=False)

        columns = ['i32', 'f']
        df2 = read_parquet(tmp, index=False, columns=columns)

        assert (df2._meta.columns == np.asarray(columns)).all(), \
            'Order of columns incorrect.'
```
I will prepare a pull request with a test.


Discussion:

Resolving patch:
diff --git a/dask/dataframe/io/parquet.py b/dask/dataframe/io/parquet.py
--- a/dask/dataframe/io/parquet.py
+++ b/dask/dataframe/io/parquet.py
@@ -118,6 +118,7 @@ def read_parquet(path, columns=None, filters=None, categories=None, index=None,
     meta = pd.DataFrame({c: pd.Series([], dtype=d)
                         for (c, d) in dtypes.items()},
                         columns=[c for c in pf.columns if c in dtypes])
+    meta = meta[list(all_columns)]
 
     for cat in categories:
         meta[cat] = pd.Series(pd.Categorical([],


Test patch:
diff --git a/dask/dataframe/io/tests/test_parquet.py b/dask/dataframe/io/tests/test_parquet.py
--- a/dask/dataframe/io/tests/test_parquet.py
+++ b/dask/dataframe/io/tests/test_parquet.py
@@ -245,6 +245,21 @@ def test_ordering():
         assert_eq(ddf, ddf2)
 
 
+def test_read_parquet_custom_columns():
+    with tmpdir() as tmp:
+        tmp = str(tmp)
+        data = pd.DataFrame({'i32': np.arange(1000, dtype=np.int32),
+                             'f': np.arange(1000, dtype=np.float64)})
+        df = dd.from_pandas(data, chunksize=50)
+        df.to_parquet(tmp)
+
+        df2 = read_parquet(tmp, columns=['i32', 'f'])
+        assert_eq(df2, df2, check_index=False)
+
+        df3 = read_parquet(tmp, columns=['f', 'i32'])
+        assert_eq(df3, df3, check_index=False)
+
+
 @pytest.mark.parametrize('df,write_kwargs,read_kwargs', [
     (pd.DataFrame({'x': [3, 2, 1]}), {}, {}),
     (pd.DataFrame({'x': ['c', 'a', 'b']}), {'object_encoding': 'utf8'}, {}),
Repository: dask/dask

Issue:
unexpected behavior with da.vstack and broadcasting
I have run into some unexpected behavior with `dask.array.vstack`, which seems like a bug to me.

```python
import dask.array as da
import numpy as np

x = da.arange(12, chunks=100).reshape((4,3))
y = da.vstack([x[:,0], x[:,1], x[:,2]]).T

ans1 = x * [0,0,1]
ans2 = y * [0,0,1]

# this is true
print(np.array_equal(x.compute() * [0,0,1], ans1.compute()))

# this is false
print(np.array_equal(ans1.compute(), ans2.compute()))

# a broadcast appears to have failed; shape is (4,9)
print(ans2.compute().shape)
```
I'm using dask v0.14 here.


Discussion:
Verified.  Also showing output here for future readers:

```python
In [18]: x.compute()
Out[18]: 
array([[ 0,  1,  2],
       [ 3,  4,  5],
       [ 6,  7,  8],
       [ 9, 10, 11]])

In [19]: y.compute()
Out[19]: 
array([[ 0,  1,  2],
       [ 3,  4,  5],
       [ 6,  7,  8],
       [ 9, 10, 11]])

In [20]: (x * [0, 0, 1]).compute()
Out[20]: 
array([[ 0,  0,  2],
       [ 0,  0,  5],
       [ 0,  0,  8],
       [ 0,  0, 11]])

In [21]: (y * [0, 0, 1]).compute()
Out[21]: 
array([[ 0,  0,  0,  0,  0,  1,  0,  0,  2],
       [ 0,  0,  3,  0,  0,  4,  0,  0,  5],
       [ 0,  0,  6,  0,  0,  7,  0,  0,  8],
       [ 0,  0,  9,  0,  0, 10,  0,  0, 11]])
```

Resolving patch:
diff --git a/dask/array/core.py b/dask/array/core.py
--- a/dask/array/core.py
+++ b/dask/array/core.py
@@ -2618,6 +2618,8 @@ def elemwise(op, *args, **kwargs):
         msg = "%s does not take the following keyword arguments %s"
         raise TypeError(msg % (op.__name__, str(sorted(set(kwargs) - set(['name', 'dtype'])))))
 
+    args = [np.asarray(a) if isinstance(a, (list, tuple)) else a for a in args]
+
     shapes = [getattr(arg, 'shape', ()) for arg in args]
     shapes = [s if isinstance(s, Iterable) else () for s in shapes]
     out_ndim = len(broadcast_shapes(*shapes))   # Raises ValueError if dimensions mismatch


Test patch:
diff --git a/dask/array/tests/test_array_core.py b/dask/array/tests/test_array_core.py
--- a/dask/array/tests/test_array_core.py
+++ b/dask/array/tests/test_array_core.py
@@ -2852,3 +2852,20 @@ def test_atop_with_numpy_arrays():
 
     s = da.sum(x)
     assert any(x is v for v in s.dask.values())
+
+
+@pytest.mark.parametrize('chunks', (100, 6))
+@pytest.mark.parametrize('other', [[0, 0, 1], [2, 1, 3], (0, 0, 1)])
+def test_elemwise_with_lists(chunks, other):
+    x = np.arange(12).reshape((4, 3))
+    d = da.arange(12, chunks=chunks).reshape((4, 3))
+
+    x2 = np.vstack([x[:, 0], x[:, 1], x[:, 2]]).T
+    d2 = da.vstack([d[:, 0], d[:, 1], d[:, 2]]).T
+
+    assert_eq(x2, d2)
+
+    x3 = x2 * other
+    d3 = d2 * other
+
+    assert_eq(x3, d3)
Repository: dask/dask

Issue:
Reshape with empty dimensions
Reshaping with zero dimensions fails

```python
def test_reshape_empty():
    x = da.ones((0, 10), chunks=(5, 5))
    y = x.reshape((0, 5, 2))
    assert_eq(x, x) 
```

I've tracked this down to the fact that this line returns a list with one empty list

```python
def intersect_chunks(...):
    ...
    old_to_new = [_intersect_1d(_breakpoints(old, new))
                  for old, new in zip(cmo, cmn)]
```

So the subsequent call to `itertools.product` returns an empty result.

I tried diving into `_intersect_1d` but had difficulty understanding the intent or the expected output.

@pitrou if you feel like looking this I would appreciate it.  Totally OK if not.  Otherwise I will resolve it later today.


Discussion:
Yes, I didn't think about zero-sized array when writing the rechunking code. It should be easy to fix, I'll take a look.

Resolving patch:
diff --git a/dask/array/rechunk.py b/dask/array/rechunk.py
--- a/dask/array/rechunk.py
+++ b/dask/array/rechunk.py
@@ -18,6 +18,7 @@
 
 from ..base import tokenize
 from .core import concatenate3, Array, normalize_chunks
+from .wrap import empty
 from .. import sharedict
 
 
@@ -484,6 +485,10 @@ def plan_rechunk(old_chunks, new_chunks, itemsize,
 def _compute_rechunk(x, chunks):
     """ Compute the rechunk of *x* to the given *chunks*.
     """
+    if x.size == 0:
+        # Special case for empty array, as the algorithm below does not behave correctly
+        return empty(x.shape, chunks=chunks, dtype=x.dtype)
+
     ndim = x.ndim
     crossed = intersect_chunks(x.chunks, chunks)
     x2 = dict()


Test patch:
diff --git a/dask/array/tests/test_array_core.py b/dask/array/tests/test_array_core.py
--- a/dask/array/tests/test_array_core.py
+++ b/dask/array/tests/test_array_core.py
@@ -788,6 +788,10 @@ def test_roll(chunks, shift, axis):
     ((4, 64), (4, 8, 4, 2), (2, 16)),
     ((4, 8, 4, 2), (2, 1, 2, 32, 2), (2, 4, 2, 2)),
     ((4, 1, 4), (4, 4), (2, 1, 2)),
+    ((0, 10), (0, 5, 2), (5, 5)),
+    ((5, 0, 2), (0, 10), (5, 2, 2)),
+    ((0,), (2, 0, 2), (4,)),
+    ((2, 0, 2), (0,), (4, 4, 4)),
 ])
 def test_reshape(original_shape, new_shape, chunks):
     x = np.random.randint(10, size=original_shape)
diff --git a/dask/array/tests/test_rechunk.py b/dask/array/tests/test_rechunk.py
--- a/dask/array/tests/test_rechunk.py
+++ b/dask/array/tests/test_rechunk.py
@@ -6,6 +6,7 @@
 
 import dask
 from dask.utils import funcname
+from dask.array.utils import assert_eq
 from dask.array.rechunk import intersect_chunks, rechunk, normalize_chunks
 from dask.array.rechunk import cumdims_label, _breakpoints, _intersect_1d
 from dask.array.rechunk import plan_rechunk, divide_to_width, merge_to_number
@@ -184,6 +185,13 @@ def test_rechunk_0d():
     assert y.compute() == a
 
 
+def test_rechunk_empty():
+    x = da.ones((0, 10), chunks=(5, 5))
+    y = x.rechunk((2, 2))
+    assert y.chunks == ((0,), (2,) * 5)
+    assert_eq(x, y)
+
+
 def test_rechunk_same():
     x = da.ones((24, 24), chunks=(4, 8))
     y = x.rechunk(x.chunks)
Repository: dask/dask

Issue:
COMPAT: pandas privacy changes in >= 0.20.0
https://github.com/pandas-dev/pandas/pull/16086

As we change what is public/private in 0.20.0. 


``pandas.tools.hashing`` -> ``pandas.util.hashing``, so need to change have a try/except on the import


This needs to change.
```
./dask/dataframe/hashing.py:# - For versions > 0.19.2, we use the provided `hash_pandas_object` function.
./dask/dataframe/hashing.py:# - For 0.19.0 through 0.19.2, we copy the definition of `hash_pandas_object`
./dask/dataframe/hashing.py:    from pandas.tools.hashing import hash_pandas_object
```


Discussion:

Resolving patch:
diff --git a/dask/dataframe/hashing.py b/dask/dataframe/hashing.py
--- a/dask/dataframe/hashing.py
+++ b/dask/dataframe/hashing.py
@@ -24,7 +24,9 @@
 # XXX: Pandas uses release branches > 0.19.0, which doesn't play well with
 # versioneer, since the release tags aren't ancestors of master. As such, we
 # need to use this hacky awfulness to check if we're > 0.19.2.
-if PANDAS_VERSION not in ('0.19.1', '0.19.2') and PANDAS_VERSION > '0.19.0+460':
+if PANDAS_VERSION >= '0.20.0':
+    from pandas.util.hashing import hash_pandas_object
+elif PANDAS_VERSION not in ('0.19.1', '0.19.2') and PANDAS_VERSION > '0.19.0+460':
     from pandas.tools.hashing import hash_pandas_object
 else:
     from pandas.types.common import (is_categorical_dtype, is_numeric_dtype,


Test patch:
Repository: dask/dask

Issue:
COMPAT: pandas privacy changes in >= 0.20.0
https://github.com/pandas-dev/pandas/pull/16086

As we change what is public/private in 0.20.0. 


``pandas.tools.hashing`` -> ``pandas.util.hashing``, so need to change have a try/except on the import


This needs to change.
```
./dask/dataframe/hashing.py:# - For versions > 0.19.2, we use the provided `hash_pandas_object` function.
./dask/dataframe/hashing.py:# - For 0.19.0 through 0.19.2, we copy the definition of `hash_pandas_object`
./dask/dataframe/hashing.py:    from pandas.tools.hashing import hash_pandas_object
```


Discussion:
This as well:

    D:\Anaconda\lib\site-packages\dask\dataframe\methods.py in <module>()
          4 import pandas as pd
          5 from pandas.api.types import is_categorical_dtype
    ----> 6 from pandas.types.concat import union_categoricals
          7 from toolz import partition
          8 

    ImportError: No module named 'pandas.types'

According to [pandas docs](https://pandas-docs.github.io/pandas-docs-travis/whatsnew.html#modules-privacy-has-changed):

 > `pandas.types` moved to `pandas.core.dtypes`

these are both private, ``pandas.api.types`` is the exposed are. this is the same as in 0.19.2 as well.

Resolving patch:
diff --git a/dask/dataframe/methods.py b/dask/dataframe/methods.py
--- a/dask/dataframe/methods.py
+++ b/dask/dataframe/methods.py
@@ -3,9 +3,13 @@
 import numpy as np
 import pandas as pd
 from pandas.api.types import is_categorical_dtype
-from pandas.types.concat import union_categoricals
 from toolz import partition
 
+from .utils import PANDAS_VERSION
+if PANDAS_VERSION >= '0.20.0':
+    from pandas.api.types import union_categoricals
+else:
+    from pandas.types.concat import union_categoricals
 
 # ---------------------------------
 # indexing


Test patch:
Repository: dask/dask

Issue:
Zero npartitions in dd.read_sql_table
if `(count * bytes_per_row / bytes_per_chunk) < 0.5` than npartitions will be 0.0 resulting a one length `divisions` array.

`np.linspace(100, 101, 0 + 1) -> array([ 100.]) -> zero partitions`

https://github.com/dask/dask/blob/328884c82d9fb9ff0ffbae630c7dbcc05681e198/dask/dataframe/io/sql.py#L113

@martindurant Using `ceil` instead of `round` solves the problem.


Discussion:
Gahhh, thanks for spotting this.
How about 
`npartitions = round(count * bytes_per_row / bytes_per_chunk) or 1`
because I'm not sure we always want to push the number or partitions higher?

Resolving patch:
diff --git a/dask/dataframe/io/sql.py b/dask/dataframe/io/sql.py
--- a/dask/dataframe/io/sql.py
+++ b/dask/dataframe/io/sql.py
@@ -110,7 +110,7 @@ def read_sql_table(table, uri, index_col, divisions=None, npartitions=None,
             q = sql.select([sql.func.count(index)]).select_from(table)
             count = pd.read_sql(q, engine)['count_1'][0]
             bytes_per_row = (head.memory_usage(deep=True, index=True)).sum() / 5
-            npartitions = round(count * bytes_per_row / bytes_per_chunk)
+            npartitions = round(count * bytes_per_row / bytes_per_chunk) or 1
         if dtype.kind == "M":
             divisions = pd.date_range(
                 start=mini, end=maxi, freq='%iS' % (


Test patch:
diff --git a/dask/dataframe/io/tests/test_sql.py b/dask/dataframe/io/tests/test_sql.py
--- a/dask/dataframe/io/tests/test_sql.py
+++ b/dask/dataframe/io/tests/test_sql.py
@@ -52,6 +52,11 @@ def test_npartitions(db):
     data = read_sql_table('test', db, columns=['name'], npartitions=6,
                           index_col="number")
     assert_eq(data, df[['name']])
+    data = read_sql_table('test', db, columns=list(df.columns),
+                          bytes_per_chunk=2**30,
+                          index_col='number')
+    assert data.npartitions == 1
+    assert (data.name.compute() == df.name).all()
 
 
 def test_divisions(db):
Repository: dask/dask

Issue:
Use absolute paths in read_* functions
In a distributed context the workers may have different working directories than the client.  In this case relative paths may be confusing.  Should we consider converting all paths on the posix file systems to absolute paths?


Discussion:
No strong opinion: is this a problem now? It would be easy to implement in LocalFileSystem._trim_filename.
I run into it from time to time.  Users do as well.  It's a common problem
in tutorials.

However it's also very easy to avoid if you understand what's going on, so
not a very high priority, mostly just a question at this point.

On Tue, Apr 25, 2017 at 3:41 PM, Martin Durant <notifications@github.com>
wrote:

> No strong opinion: is this a problem now? It would be easy to implement in
> LocalFileSystem._trim_filename.
>
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/dask/dask/issues/2256#issuecomment-297142486>, or mute
> the thread
> <https://github.com/notifications/unsubscribe-auth/AASszDL1bO4OpV5VC1VSgCPExhDoWOZZks5rzkxfgaJpZM4NHvGc>
> .
>


Resolving patch:
diff --git a/dask/bytes/local.py b/dask/bytes/local.py
--- a/dask/bytes/local.py
+++ b/dask/bytes/local.py
@@ -28,13 +28,13 @@ def __init__(self, **storage_options):
         storage_options: key-value
             May be credentials, or other configuration specific to the backend.
         """
-        # no configuration necessary
-        pass
+        self.cwd = os.getcwd()
 
-    @staticmethod
-    def _trim_filename(fn):
-        so = infer_storage_options(fn)
-        return so['path']
+    def _trim_filename(self, fn):
+        path = infer_storage_options(fn)['path']
+        if not os.path.isabs(path):
+            path = os.path.normpath(os.path.join(self.cwd, path))
+        return path
 
     def glob(self, path):
         """For a template path, return matching files"""


Test patch:
diff --git a/dask/bytes/tests/test_local.py b/dask/bytes/tests/test_local.py
--- a/dask/bytes/tests/test_local.py
+++ b/dask/bytes/tests/test_local.py
@@ -205,9 +205,9 @@ def test_compression_text(fmt):
 
 @pytest.mark.parametrize('fmt', list(compression.seekable_files))
 def test_getsize(fmt):
-    fs = LocalFileSystem()
     compress = compression.compress[fmt]
     with filetexts({'.tmp.getsize': compress(b'1234567890')}, mode='b'):
+        fs = LocalFileSystem()
         assert fs.logical_size('.tmp.getsize', fmt) == 10
 
 
@@ -319,6 +319,22 @@ def test_py2_local_bytes(tmpdir):
         assert all(isinstance(line, unicode) for line in f)
 
 
+def test_abs_paths(tmpdir):
+    tmpdir = str(tmpdir)
+    here = os.getcwd()
+    os.chdir(tmpdir)
+    with open('tmp', 'w') as f:
+        f.write('hi')
+    out = LocalFileSystem().glob('*')
+    assert len(out) == 1
+    assert os.sep in out[0]
+    assert tmpdir in out[0] and 'tmp' in out[0]
+
+    fs = LocalFileSystem()
+    os.chdir(here)
+    assert fs.open('tmp', 'r').read() == 'hi'
+
+
 try:
     # used only in test_with_urls - may be more generally useful
     import pathlib