internlm/SWE-Fixer-Train-110K huggingface dataset:
Repository: liberapay/liberapay.com

Issue:
[feature] Pledge on Mastodon account
As of now, one can only pledge to accounts on Twitter, GitHub and Bitbucket.

After searching in the issues. Several mention Mastodon but none is about the pledge feature.


Discussion:
Is there a technical reason that blocks this? Or this is only because more hot stuff is in the list?
Thus, is the feature valuable and would a patch be accepted?
FWIW, I'd see this as a great improvement.
> Thus, is the feature valuable and would a patch be accepted?

Seeing how @Changaco tagged this the way he did, I think a PR would be accepted.
I wonder if it could be generalized to any ActivityPub account.
I just remembered that this is actually an upstream issue: <https://github.com/tootsuite/mastodon/issues/4588>.
The upstream issue has been fixed. The documentation for the new API endpoint is at https://docs.joinmastodon.org/methods/accounts/#lookup

Resolving patch:
diff --git a/liberapay/elsewhere/mastodon.py b/liberapay/elsewhere/mastodon.py
--- a/liberapay/elsewhere/mastodon.py
+++ b/liberapay/elsewhere/mastodon.py
@@ -33,8 +33,7 @@ def example_account_address(self, _):
     api_paginator = header_links_paginator()
     api_url = 'https://{domain}/api/v1'
     api_user_info_path = '/accounts/{user_id}'
-    # https://github.com/tootsuite/mastodon/issues/4588
-    # api_user_name_info_path = '/accounts/search?q={user_name}@{domain}'
+    api_user_name_info_path = '/accounts/lookup?acct={user_name}'
     api_user_self_info_path = '/accounts/verify_credentials'
     api_follows_path = '/accounts/{user_id}/following'
     ratelimit_headers_prefix = 'x-ratelimit-'
diff --git a/liberapay/models/exchange_route.py b/liberapay/models/exchange_route.py
--- a/liberapay/models/exchange_route.py
+++ b/liberapay/models/exchange_route.py
@@ -193,7 +193,11 @@ def invalidate(self, obj=None):
                 try:
                     source = stripe.Source.retrieve(self.address).detach()
                 except stripe.error.InvalidRequestError as e:
-                    if "does not appear to be currently attached" in str(e):
+                    ignore = (
+                        "does not appear to be currently attached" in str(e) or
+                        "No such source: " in str(e)
+                    )
+                    if ignore:
                         pass
                     else:
                         raise


Test patch:
Repository: liberapay/liberapay.com

Issue:
Enabling or disabling a specific visibility level as a creator
This issue is for the upcoming feature mentioned in <https://medium.com/liberapay-blog/lifting-the-veil-of-anonymity-479dadd369be>.
Patrons page doesn't mention the lack of support for secret donations through PayPal
I just clicked the option to explictly not show who my patrons are in the settings. On the settings page it shows "You've chosen not to see who your patrons are." However on the donation page it shows "This donation won't be secret, you will appear in bjorn3's private list of patrons." Which of those two statements is true?


Discussion:

Both statements are true, but the first one is incomplete. Secret donations are only possible if a Stripe account is connected in https://liberapay.com/about/me/payment
Right, I do get emails from paypal with the email address and name of the person donating. In any case the message on the donation page before I chose an option is incorrect then. That one did talk about donations being secret due to me not having chosen anything.
> In any case the message on the donation page before I chose an option is incorrect then. That one did talk about donations being secret due to me not having chosen anything.

This bug is now fixed.

Resolving patch:
diff --git a/liberapay/utils/__init__.py b/liberapay/utils/__init__.py
--- a/liberapay/utils/__init__.py
+++ b/liberapay/utils/__init__.py
@@ -538,8 +538,8 @@ def word(mapping, k, pattern=r'^\w+$', unicode=False):
     return r
 
 
-FALSEISH = {'0', 'f', 'false', 'n', 'no'}
-TRUEISH = {'1', 't', 'true', 'y', 'yes'}
+FALSEISH = {'0', 'f', 'false', 'n', 'no', 'off'}
+TRUEISH = {'1', 't', 'true', 'y', 'yes', 'on'}
 NULLISH = {'', 'null', 'none'}
 
 


Test patch:
Repository: liberapay/liberapay.com

Issue:
[Feature Request] Always put your primary email address at the top
I first added the "@qq.com" email address, and then added the "@protonmail.com" email address and set it as the new primary email address.
But it was ranked second. Obviously, this order is in chronological order of adding. But the primary email address is already the "primary" address, so why shouldn't it be ranked first?

![screenshot](https://github.com/liberapay/liberapay.com/assets/42690037/c63a283d-fdfc-441a-bd48-e1ae3509a7de)



Discussion:

Resolving patch:
diff --git a/liberapay/payin/paypal.py b/liberapay/payin/paypal.py
--- a/liberapay/payin/paypal.py
+++ b/liberapay/payin/paypal.py
@@ -224,7 +224,7 @@ def record_order_result(db, payin, order):
             refund_amount = refund['amount']
             refund_amount = Money(refund_amount['value'], refund_amount['currency_code'])
             reversed_amount += refund_amount
-            refund_description = refund['note_to_payer']
+            refund_description = refund.get('note_to_payer')
             refund_status = REFUND_STATUSES_MAP[refund['status']]
             refund_error = refund.get('status_details', {}).get('reason')
             payin_refund = record_payin_refund(


Test patch:
Repository: dask/dask

Issue:
Add `numpy.where` equivalent
If I'm just missing this, I'm sorry I didn't catch it and am sorry to trouble you over it. If not, my proposal is below. Please let me know your thoughts. Also, I don't know all the details of your system. So, if there is something I have missed, please let me know.

I think it would be really cool to have a `numpy.where` type function in `dask`, especially, as the arrays themselves cannot be set. Though, using a `numpy.where` approach maybe setting in certain context would be acceptable, as well. I'm thinking this could just behave as a simple branch that is inserted into the the graph. Additionally, if the condition is trivial like a single bool, this could result in removal of the unused portion of the graph.



Discussion:

Resolving patch:
diff --git a/dask/array/core.py b/dask/array/core.py
--- a/dask/array/core.py
+++ b/dask/array/core.py
@@ -950,6 +950,11 @@ def choose(a, choices):
     return elemwise(variadic_choose, a, *choices)
 
 
+@wraps(np.where)
+def where(condition, x, y):
+    return choose(condition, [y, x])
+
+
 @wraps(chunk.coarsen)
 def coarsen(reduction, x, axes):
     if not all(bd % div == 0 for i, div in axes.items()


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
@@ -339,13 +339,21 @@ def test_norm():
 def test_choose():
     x = np.random.randint(10, size=(15, 16))
     d = from_array(x, blockshape=(4, 5))
-    y = np.random.randint(10, size=15)
-    e = from_array(y, blockshape=(4,))
 
     assert eq(choose(d > 5, [0, d]), np.choose(x > 5, [0, x]))
     assert eq(choose(d > 5, [-d, d]), np.choose(x > 5, [-x, x]))
 
 
+def test_where():
+    x = np.random.randint(10, size=(15, 16))
+    d = from_array(x, blockshape=(4, 5))
+    y = np.random.randint(10, size=15)
+    e = from_array(y, blockshape=(4,))
+
+    assert eq(where(d > 5, d, 0), np.where(x > 5, x, 0))
+    assert eq(where(d > 5, d, -e[:, None]), np.where(x > 5, x, -y[:, None]))
+
+
 def test_coarsen():
     x = np.random.randint(10, size=(24, 24))
     d = from_array(x, blockshape=(4, 8))
Repository: dask/dask

Issue:
Support for __len__
NumPy enables `__len__` to return the shape at axis `0`. If the array is a dimensionless scalar, it will fail with `TypeError: len() of unsized object`. Hopefully, this is an easy add.



Discussion:
It is an easy add.  Do you have any interest in adding it?  I've heard that once you get a contributor hooked they stay on for a while :)

Sure, I can try. I'll take a look at it this weekend.


Resolving patch:
diff --git a/dask/array/core.py b/dask/array/core.py
--- a/dask/array/core.py
+++ b/dask/array/core.py
@@ -454,6 +454,9 @@ def numblocks(self):
     def shape(self):
         return tuple(map(sum, self.blockdims))
 
+    def __len__(self):
+        return sum(self.blockdims[0])
+
     @property
     def dtype(self):
         if self.shape:


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
@@ -137,6 +137,10 @@ def test_Array():
 
     assert a.blockdims == ((100,) * 10, (100,) * 10)
 
+    assert a.shape == shape
+
+    assert len(a) == shape[0]
+
 
 def test_uneven_blockdims():
     a = Array({}, 'x', shape=(10, 10), blockshape=(3, 3))
Repository: dask/dask

Issue:
slices with stop=0 give incorrect result
```
In [4]: import numpy as np

In [5]: import dask.array as da

In [6]: da.ones(10, blockshape=(10,))[:0].compute()
Out[6]: array([ 1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.])

In [7]: np.ones(10)[:0]
Out[7]: array([], dtype=float64)
```



Discussion:

Resolving patch:
diff --git a/dask/array/core.py b/dask/array/core.py
--- a/dask/array/core.py
+++ b/dask/array/core.py
@@ -352,6 +352,8 @@ def rec_concatenate(arrays, axis=0):
            [1, 2, 1, 2],
            [1, 2, 1, 2]])
     """
+    if not arrays:
+        return np.array([])
     if isinstance(arrays, Iterator):
         arrays = list(arrays)
     if isinstance(arrays[0], Iterator):
diff --git a/dask/array/slicing.py b/dask/array/slicing.py
--- a/dask/array/slicing.py
+++ b/dask/array/slicing.py
@@ -317,7 +317,7 @@ def _slice_1d(dim_shape, lengths, index):
     step = index.step or 1
     if step > 0:
         start = index.start or 0
-        stop = index.stop or dim_shape
+        stop = index.stop if index.stop is not None else dim_shape
     else:
         start = index.start or dim_shape - 1
         start = dim_shape - 1 if start >= dim_shape else start


Test patch:
diff --git a/dask/array/tests/test_slicing.py b/dask/array/tests/test_slicing.py
--- a/dask/array/tests/test_slicing.py
+++ b/dask/array/tests/test_slicing.py
@@ -1,9 +1,21 @@
 import dask
 import dask.array as da
+from dask.array import Array
 from dask.array.slicing import slice_array, _slice_1d, take
 from operator import getitem
 import numpy as np
-from pprint import pprint
+
+
+def eq(a, b):
+    if isinstance(a, da.Array):
+        a = a.compute(get=dask.get)
+    if isinstance(b, da.Array):
+        b = b.compute(get=dask.get)
+
+    c = a == b
+    if isinstance(c, np.ndarray):
+        c = c.all()
+    return c
 
 
 def test_slice_1d():
@@ -324,3 +336,10 @@ def test_slicing_and_blockdims():
     o = da.ones((24, 16), blockdims=((4, 8, 8, 4), (2, 6, 6, 2)))
     t = o[4:-4, 2:-2]
     assert t.blockdims == ((8, 8), (6, 6))
+
+
+def test_slice_stop_0():
+    # from gh-125
+    a = da.ones(10, blockshape=(10,))[:0].compute()
+    b = np.ones(10)[:0]
+    assert eq(a, b)
Repository: dask/dask

Issue:
Default ThreadPool for dask.array
Currently we spin up a separate ThreadPool for each `dask.array` computation.  This adds about 100ms overhead to every full computation (not every task.) This can add up for applications, like xray, that might manage operations on many small dask.arrays.

Do we want to have a single default `ThreadPool`?  Are there any costs to this?



Discussion:
Threads use up about 8MB of memory each, according to [Effective Python](http://www.amazon.com/Effective-Python-Specific-Software-Development/dp/0134034287/ref=sr_1_1?ie=UTF8&qid=1429042387&sr=8-1&keywords=effective+python) (which I just read on an airplane and highly recommend!). These days, typical machines have ~1-4GB of memory per CPU, so IMHO allocating 1-2 threads/CPU would pretty much be rounding error and a really nice idea from a usability perspective.

OTOH, it's also pretty easy for applications like xray to create the global threadpool as a resource at import time, too.

We did get a complaint about this in pandas: https://github.com/pydata/pandas/issues/9394


Resolving patch:
diff --git a/dask/threaded.py b/dask/threaded.py
--- a/dask/threaded.py
+++ b/dask/threaded.py
@@ -6,16 +6,15 @@
 from __future__ import absolute_import, division, print_function
 
 from multiprocessing.pool import ThreadPool
-import psutil
 from .async import get_async, inc, add
 from .compatibility import Queue
 from .context import _globals
 
 
-NUM_CPUS = psutil.cpu_count()
+default_pool = ThreadPool()
 
 
-def get(dsk, result, nthreads=NUM_CPUS, cache=None, debug_counts=None, **kwargs):
+def get(dsk, result, cache=None, debug_counts=None, **kwargs):
     """ Threaded cached implementation of dask.get
 
     Parameters
@@ -44,19 +43,11 @@ def get(dsk, result, nthreads=NUM_CPUS, cache=None, debug_counts=None, **kwargs)
     pool = _globals['pool']
 
     if pool is None:
-        pool = ThreadPool(nthreads)
-        cleanup = True
-    else:
-        cleanup = False
+        pool = default_pool
 
     queue = Queue()
-    try:
-        results = get_async(pool.apply_async, nthreads, dsk, result,
-                            cache=cache, debug_counts=debug_counts,
-                            queue=queue, **kwargs)
-    finally:
-        if cleanup:
-            pool.close()
-            pool.join()
+    results = get_async(pool.apply_async, len(pool._pool), dsk, result,
+                        cache=cache, debug_counts=debug_counts,
+                        queue=queue, **kwargs)
 
     return results


Test patch:
Repository: dask/dask

Issue:
failing test case for repeated insertion
Just stumbled upon this with the xray test suite -- it's almost certainly related to the last round of slicing fixes.



Discussion:

Resolving patch:
diff --git a/dask/array/core.py b/dask/array/core.py
--- a/dask/array/core.py
+++ b/dask/array/core.py
@@ -560,7 +560,7 @@ def __init__(self, dask, name, chunks, shape=None, dtype=None):
         self.dask = dask
         self.name = name
         self.chunks = normalize_chunks(chunks, shape)
-        if isinstance(dtype, (str, list)):
+        if dtype is not None:
             dtype = np.dtype(dtype)
         self._dtype = dtype
 
@@ -1496,8 +1496,9 @@ def insert(arr, obj, values, axis):
     values_breaks = np.cumsum(counts[counts > 0])
     split_values = split_at_breaks(values, values_breaks, axis)
 
-    interleaved = interleave([split_arr, split_values])
-    return concatenate(list(interleaved), axis=axis)
+    interleaved = list(interleave([split_arr, split_values]))
+    interleaved = [i for i in interleaved if i.nbytes]
+    return concatenate(interleaved, axis=axis)
 
 
 @wraps(chunk.broadcast_to)


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
@@ -466,6 +466,13 @@ def test_insert():
     assert raises(IndexError, lambda: insert(a, [3], -1, axis=-3))
 
 
+def test_multi_insert():
+    z = np.random.randint(10, size=(1, 2))
+    c = from_array(z, chunks=(1, 2))
+    assert eq(np.insert(np.insert(z, [0, 1], -1, axis=0), [1], -1, axis=1),
+              insert(insert(c, [0, 1], -1, axis=0), [1], -1, axis=1))
+
+
 def test_broadcast_to():
     x = np.random.randint(10, size=(5, 1, 6))
     a = from_array(x, chunks=(3, 1, 3))
@@ -889,3 +896,8 @@ def test_size():
 def test_nbytes():
     x = da.ones((10, 2), chunks=(3, 1))
     assert x.nbytes == np.array(x).nbytes
+
+
+def test_Array_normalizes_dtype():
+    x = da.ones((3,), chunks=(1,), dtype=int)
+    assert isinstance(x.dtype, np.dtype)
Repository: dask/dask

Issue:
rechunk should gracefully handle an empty dictionary as an argument
Currently, it raises `StopIteration`:

```
>>> dask.array.from_array(np.arange(5), 5).rechunk({})
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-20-2ac5cd3dad0f> in <module>()
----> 1 dask.array.from_array(np.arange(5), 5).rechunk({})

/Users/shoyer/dev/dask/dask/array/core.pyc in rechunk(self, chunks)
    826     def rechunk(self, chunks):
    827         from .rechunk import rechunk
--> 828         return rechunk(self, chunks)
    829 
    830 

/Users/shoyer/dev/dask/dask/array/rechunk.pyc in rechunk(x, chunks)
    192     """
    193     if isinstance(chunks, dict):
--> 194         if isinstance(next(iter(chunks.values())), int):
    195             chunks = blockshape_dict_to_tuple(x.chunks, chunks)
    196         else:

StopIteration: 
```



Discussion:

Resolving patch:
diff --git a/dask/array/rechunk.py b/dask/array/rechunk.py
--- a/dask/array/rechunk.py
+++ b/dask/array/rechunk.py
@@ -11,7 +11,8 @@
 from operator import getitem, add
 import numpy as np
 from toolz import merge, accumulate
-from dask.array.core import rec_concatenate, Array, normalize_chunks
+
+from .core import rec_concatenate, Array, normalize_chunks
 
 
 rechunk_names  = ('rechunk-%d' % i for i in count(1))
@@ -191,11 +192,13 @@ def rechunk(x, chunks):
     chunks:  the new block dimensions to create
     """
     if isinstance(chunks, dict):
-        if isinstance(next(iter(chunks.values())), int):
+        if not chunks or isinstance(next(iter(chunks.values())), int):
             chunks = blockshape_dict_to_tuple(x.chunks, chunks)
         else:
             chunks = blockdims_dict_to_tuple(x.chunks, chunks)
     chunks = normalize_chunks(chunks, x.shape)
+    if not len(chunks) == x.ndim or tuple(map(sum, chunks)) != x.shape:
+        raise ValueError("Provided chunks are not consistent with shape")
 
     crossed = intersect_chunks(x.chunks, chunks)
     x2 = dict()


Test patch:
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
@@ -3,6 +3,7 @@
 from dask.array.rechunk import intersect_chunks, rechunk, normalize_chunks
 from dask.array.rechunk import cumdims_label, _breakpoints, _intersect_1d
 import dask.array as da
+from dask.utils import raises
 
 
 def test_rechunk_internals_1():
@@ -154,3 +155,9 @@ def test_rechunk_with_dict():
     x = da.ones((24, 24), chunks=(4, 8))
     y = x.rechunk(chunks={0: (12, 12)})
     assert y.chunks == ((12, 12), (8, 8, 8))
+
+
+def test_rechunk_with_empty_input():
+    x = da.ones((24, 24), chunks=(4, 8))
+    assert x.rechunk(chunks={}).chunks == x.chunks
+    assert raises(ValueError, lambda: x.rechunk(chunks=()))
Repository: dask/dask

Issue:
Wrong result for unevenly sliced chunks
``` python
In [1]: import dask.array as da

In [2]: x = da.ones(20, chunks=5)

In [3]: x[::2].chunks
((2, 2, 2, 2),)
```

Should be 

```
((3, 2, 3, 2),)
```



Discussion:

Resolving patch:
diff --git a/dask/array/slicing.py b/dask/array/slicing.py
--- a/dask/array/slicing.py
+++ b/dask/array/slicing.py
@@ -1,6 +1,7 @@
 from itertools import count, product
 from toolz import merge, first, accumulate
 from operator import getitem, add
+from math import ceil
 from ..compatibility import long
 import numpy as np
 
@@ -560,7 +561,7 @@ def new_blockdim(dim_shape, lengths, index):
     [4]
 
     >>> new_blockdim(100, [20, 10, 20, 10, 40], slice(90, 10, -2))
-    [15, 5, 10, 5, 4]
+    [16, 5, 10, 5, 4]
     """
     if isinstance(index, list):
         return [len(index)]
@@ -570,7 +571,7 @@ def new_blockdim(dim_shape, lengths, index):
                 for i, slc in pairs]
     if isinstance(index, slice) and index.step and index.step < 0:
         slices = slices[::-1]
-    return [(slc.stop - slc.start) // slc.step for slc in slices]
+    return [int(ceil((1. * slc.stop - slc.start) / slc.step)) for slc in slices]
 
 
 def replace_ellipsis(n, index):


Test patch:
diff --git a/dask/array/tests/test_slicing.py b/dask/array/tests/test_slicing.py
--- a/dask/array/tests/test_slicing.py
+++ b/dask/array/tests/test_slicing.py
@@ -2,7 +2,7 @@
 from dask.compatibility import skip
 import dask.array as da
 from dask.array import Array
-from dask.array.slicing import slice_array, _slice_1d, take
+from dask.array.slicing import slice_array, _slice_1d, take, new_blockdim
 from operator import getitem
 import numpy as np
 from toolz import merge
@@ -437,3 +437,11 @@ def test_multiple_list_slicing():
     x = np.random.rand(6, 7, 8)
     a = da.from_array(x, chunks=(3, 3, 3))
     assert eq(x[:, [0, 1, 2]][[0, 1]], a[:, [0, 1, 2]][[0, 1]])
+
+
+def test_uneven_chunks():
+    assert da.ones(20, chunks=5)[::2].chunks == ((3, 2, 3, 2),)
+
+
+def test_new_blockdim():
+    assert new_blockdim(20, [5, 5, 5, 5], slice(0, None, 2)) == [3, 2, 3, 2]
Repository: dask/dask

Issue:
dot_graph does not work in stable version
I try to generate visual graphs as [described in documentation](http://dask.pydata.org/en/latest/inspect.html), but get:
`'module' object has no attribute 'to_pydot'`

The graphviz is installed with homebrew. Dask is installed from conda (latest stable release):

```
In [15]: dask.__version__

Out[15]: '0.5.0'
```

The code and traceback are below (I had to replace `blockshape` with `chunks`, otherwise it did not create task graph):

``` python
In [1]: 
import dask.array as da
from dask.dot import dot_graph
In [2]:

x = da.ones((5, 15), chunks=(5, 5))
In [5]:

d = (x + 1).dask
In [6]:

dot_graph(d)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-6-c797e633866d> in <module>()
----> 1 dot_graph(d)

/Users/koldunov/miniconda/lib/python2.7/site-packages/dask/dot.pyc in dot_graph(d, filename, **kwargs)
     73 def dot_graph(d, filename='mydask', **kwargs):
     74     dg = to_networkx(d, **kwargs)
---> 75     write_networkx_to_dot(dg, filename=filename)
     76 
     77 

/Users/koldunov/miniconda/lib/python2.7/site-packages/dask/dot.pyc in write_networkx_to_dot(dg, filename)
     61 def write_networkx_to_dot(dg, filename='mydask'):
     62     import os
---> 63     p = nx.to_pydot(dg)
     64     p.set_rankdir('BT')
     65     with open(filename + '.dot', 'w') as f:

AttributeError: 'module' object has no attribute 'to_pydot'
```



Discussion:
Hrm, perhaps one also needs to install the pydot library

```
pip install pydot
```

We haven't included these in the dependencies because the visualize stuff is useful but not essential.  If this fixes the problem let me know and I'll improve the error message to direct future users to the solution.

Yes, that fixes  the problem, thanks a lot! I am not sure if you also have to replace `blockshape` with `chunks` in documentation, otherwise the example did not work.

Where did you find `blockshape`.  Yes, these should all be replaced with `chunks`.

Ah, I see that you referred to the link in the docs.  Fixing.


Resolving patch:
diff --git a/dask/dot.py b/dask/dot.py
--- a/dask/dot.py
+++ b/dask/dot.py
@@ -60,7 +60,11 @@ def to_networkx(d, data_attributes=None, function_attributes=None):
 
 def write_networkx_to_dot(dg, filename='mydask'):
     import os
-    p = nx.to_pydot(dg)
+    try:
+        p = nx.to_pydot(dg)
+    except AttributeError:
+        raise ImportError("Can not find pydot module. Please install.\n"
+                          "    pip install pydot")
     p.set_rankdir('BT')
     with open(filename + '.dot', 'w') as f:
         f.write(p.to_string())


Test patch: