Repository: sympy/sympy
Pull request: Derivative._sort_variable_count correction
AppliedUndef and Symbols commute; derivatives don't commute
with symbols or functions that are contained in the derivative.
Anything else is assumed to behave like derivatives in
terms of sorting.

Here is an example where order of differentiation matters:
```python
>>> f = f(x)
>>> df = f.diff(x)
>>> (f+x*df).diff(x, df)
2
>>> (f+x*df)*diff(df, x)
1
```
If the x were changed to y in `diff(x, df)` the order would not matter.
<!-- Your title above should be a short description of what
was changed. Do not include the issue number in the title. -->

#### References to other Issues or PRs
<!-- If this pull request fixes an issue, write "Fixes #NNNN" in that exact
format, e.g. "Fixes #1234". See
https://github.com/blog/1506-closing-issues-via-pull-requests .-->

fixes #15028

#### Brief description of what is fixed or changed

Current master groups wrt according to `foo.is_symbol` and sorts *all* groups. It should not sort the non-`is_symbol` groups. If we don't care about being canonical, then simply not sorting the non-symbols is a sufficient fix. 

#### Other comments
Symbol and AppliedUndef should be sorted as one group when they are contiguous since `foo.diff(x, f(x))` is the same as `foo.diff(f(x), x)`. In addition, if there are no free symbols in common between two objects then the two objects should be re-orderable.

In order to make a canonically sorted list of variables Iput `topological_sort` to work after trying several other methods of ordering wrt variables such that an object is free to appear before another if it is not blocked by free symbols or the rules of differentiation. e.g. `f(x,y).diff(y), y` cannot have the order reversed, but `x` can appear before `f(x,y).diff(y)`.

#### Release Notes

<!-- Write the release notes for this release below. See
https://github.com/sympy/sympy/wiki/Writing-Release-Notes for more information
on how to write release notes. The bot will check your release notes
automatically to see if they are formatted correctly. -->

<!-- BEGIN RELEASE NOTES -->
* core
  * Derivative._sort_variable_count is more canonical and preserves the order of derivatives and symbols when necessary
<!-- END RELEASE NOTES -->


Problem statement:
better canonicalization of variables of Derivative
Better canonicalization of `Derivative._sort_variable_count` will be had if any symbols, appearing after functions, that are not in the free symbols of the function, appear before the functions: `Derivative(f(x, y), x, f(y), x)` should equal `Derivative(f(x, y), x, x, f(y))`.

Hints from issue discussion:
This sorting
```python
>>> Derivative._sort_variable_count([(f(x).diff(x),1),(x,1),(f(x),1),(x,1)])
[(Derivative(f(x), x), 1), (x, 1), (f(x), 1), (x, 1)]
```
could be this
```
[(f(x), 1), (Derivative(f(x), x), 1), (x, 2)]
```
but the only harm here is that it is not canonical. The following ordering is wrong, however:
```
>>> y = f(x)
>>> Derivative._sort_variable_count([(f(y).diff(y),1),(y,1)])
[(f(x), 1), (Derivative(f(f(x)), f(x)), 1)]
```
(The variable of differentiation should not reorder with a derivative having that same variable of differentiation.)

Commit message: _sort_variable_count: watch for noncommuting wrt

AppliedUndef and Symbols commute; derivatives don't commute
with symbols or functions that are contained in the derivative.
Anything else is assumed not to behave like derivatives in
terms of sorting.

Patch to review:
diff --git a/sympy/core/function.py b/sympy/core/function.py
index 97099ce6d9ca..eafd20745c94 100644
--- a/sympy/core/function.py
+++ b/sympy/core/function.py
@@ -1298,72 +1298,101 @@ def _remove_derived_once(cls, v):
         return [i[0] if i[1] == 1 else i for i in v]
 
     @classmethod
-    def _sort_variable_count(cls, varcounts):
+    def _sort_variable_count(cls, vc):
         """
-        Sort (variable, count) pairs by variable, but disallow sorting of non-symbols.
+        Sort (variable, count) pairs into canonical order while
+        retaining order of variables that do not commute during
+        differentiation:
 
-        The count is not sorted. It is kept in the same order as the input
-        after sorting by variable.
-
-        When taking derivatives, the following rules usually hold:
-
-        * Derivative wrt different symbols commute.
-        * Derivative wrt different non-symbols commute.
-        * Derivatives wrt symbols and non-symbols don't commute.
+        * symbols and functions commute with each other
+        * derivatives commute with each other
+        * a derivative doesn't commute with anything it contains
+        * any other object is not allowed to commute if it has
+          free symbols in common with another object
 
         Examples
         ========
 
-        >>> from sympy import Derivative, Function, symbols
+        >>> from sympy import Derivative, Function, symbols, cos
         >>> vsort = Derivative._sort_variable_count
         >>> x, y, z = symbols('x y z')
         >>> f, g, h = symbols('f g h', cls=Function)
 
-        >>> vsort([(x, 3), (y, 2), (z, 1)])
-        [(x, 3), (y, 2), (z, 1)]
-
-        >>> vsort([(h(x), 1), (g(x), 1), (f(x), 1)])
-        [(f(x), 1), (g(x), 1), (h(x), 1)]
+        Contiguous items are collapsed into one pair:
 
-        >>> vsort([(z, 1), (y, 2), (x, 3), (h(x), 1), (g(x), 1), (f(x), 1)])
-        [(x, 3), (y, 2), (z, 1), (f(x), 1), (g(x), 1), (h(x), 1)]
+        >>> vsort([(x, 1), (x, 1)])
+        [(x, 2)]
+        >>> vsort([(y, 1), (f(x), 1), (y, 1), (f(x), 1)])
+        [(y, 2), (f(x), 2)]
 
-        >>> vsort([(x, 1), (f(x), 1), (y, 1), (f(y), 1)])
-        [(x, 1), (f(x), 1), (y, 1), (f(y), 1)]
+        Ordering is canonical.
 
-        >>> vsort([(y, 1), (x, 2), (g(x), 1), (f(x), 1), (z, 1), (h(x), 1), (y, 2), (x, 1)])
-        [(x, 2), (y, 1), (f(x), 1), (g(x), 1), (z, 1), (h(x), 1), (x, 1), (y, 2)]
+        >>> def vsort0(*v):
+        ...     # docstring helper to
+        ...     # change vi -> (vi, 0), sort, and return vi vals
+        ...     return [i[0] for i in vsort([(i, 0) for i in v])]
 
-        >>> vsort([(z, 1), (y, 1), (f(x), 1), (x, 1), (f(x), 1), (g(x), 1)])
-        [(y, 1), (z, 1), (f(x), 1), (x, 1), (f(x), 1), (g(x), 1)]
+        >>> vsort0(y, x)
+        [x, y]
+        >>> vsort0(g(y), g(x), f(y))
+        [f(y), g(x), g(y)]
 
-        >>> vsort([(z, 1), (y, 2), (f(x), 1), (x, 2), (f(x), 2), (g(x), 1), (z, 2), (z, 1), (y, 1), (x, 1)])
-        [(y, 2), (z, 1), (f(x), 1), (x, 2), (f(x), 2), (g(x), 1), (x, 1), (y, 1), (z, 2), (z, 1)]
+        Symbols are sorted as far to the left as possible but never
+        move to the left of a derivative having the same symbol in
+        its variables; the same applies to AppliedUndef which are
+        always sorted after Symbols:
 
+        >>> dfx = f(x).diff(x)
+        >>> assert vsort0(dfx, y) == [y, dfx]
+        >>> assert vsort0(dfx, x) == [dfx, x]
         """
-        sorted_vars = []
-        symbol_part = []
-        non_symbol_part = []
-        for (v, c) in varcounts:
-            if not v.is_symbol:
-                if len(symbol_part) > 0:
-                    sorted_vars.extend(sorted(symbol_part,
-                                              key=lambda i: default_sort_key(i[0])))
-                    symbol_part = []
-                non_symbol_part.append((v, c))
+        from sympy.utilities.iterables import uniq, topological_sort
+        if not vc:
+            return []
+        vc = list(vc)
+        if len(vc) == 1:
+            return [Tuple(*vc[0])]
+        V = list(range(len(vc)))
+        E = []
+        v = lambda i: vc[i][0]
+        D = Dummy()
+        def _block(d, v, wrt=False):
+            # return True if v should not come before d else False
+            if d == v:
+                return wrt
+            if d.is_Symbol:
+                return False
+            if isinstance(d, Derivative):
+                # a derivative blocks if any of it's variables contain
+                # v; the wrt flag will return True for an exact match
+                # and will cause an AppliedUndef to block if v is in
+                # the arguments
+                if any(_block(k, v, wrt=True)
+                        for k in [vi[0] for vi in d.variable_count]):
+                    return True
+                return False
+            if not wrt and isinstance(d, AppliedUndef):
+                return False
+            if v.is_Symbol:
+                return v in d.free_symbols
+            if isinstance(v, AppliedUndef):
+                return _block(d.xreplace({v: D}), D)
+            return d.free_symbols & v.free_symbols
+        for i in range(len(vc)):
+            for j in range(i):
+                if _block(v(j), v(i)):
+                    E.append((j,i))
+        # this is the default ordering to use in case of ties
+        O = dict(zip(ordered(uniq([i for i, c in vc])), range(len(vc))))
+        ix = topological_sort((V, E), key=lambda i: O[v(i)])
+        # merge counts of contiguously identical items
+        merged = []
+        for v, c in [vc[i] for i in ix]:
+            if merged and merged[-1][0] == v:
+                merged[-1][1] += c
             else:
-                if len(non_symbol_part) > 0:
-                    sorted_vars.extend(sorted(non_symbol_part,
-                                              key=lambda i: default_sort_key(i[0])))
-                    non_symbol_part = []
-                symbol_part.append((v, c))
-        if len(non_symbol_part) > 0:
-            sorted_vars.extend(sorted(non_symbol_part,
-                                      key=lambda i: default_sort_key(i[0])))
-        if len(symbol_part) > 0:
-            sorted_vars.extend(sorted(symbol_part,
-                                      key=lambda i: default_sort_key(i[0])))
-        return [Tuple(*i) for i in sorted_vars]
+                merged.append([v, c])
+        return [Tuple(*i) for i in merged]
 
     def _eval_is_commutative(self):
         return self.expr.is_commutative
diff --git a/sympy/core/tests/test_function.py b/sympy/core/tests/test_function.py
index a5952534af03..8541f263c014 100644
--- a/sympy/core/tests/test_function.py
+++ b/sympy/core/tests/test_function.py
@@ -1,9 +1,11 @@
 from sympy import (Lambda, Symbol, Function, Derivative, Subs, sqrt,
         log, exp, Rational, Float, sin, cos, acos, diff, I, re, im,
         E, expand, pi, O, Sum, S, polygamma, loggamma, expint,
-        Tuple, Dummy, Eq, Expr, symbols, nfloat, Piecewise, Indexed)
+        Tuple, Dummy, Eq, Expr, symbols, nfloat, Piecewise, Indexed,
+        Matrix, Basic)
 from sympy.utilities.pytest import XFAIL, raises
 from sympy.abc import t, w, x, y, z
+from sympy.core.basic import _aresame
 from sympy.core.function import PoleError, _mexpand
 from sympy.core.sympify import sympify
 from sympy.sets.sets import FiniteSet
@@ -643,30 +645,82 @@ def test_straight_line():
 
 def test_sort_variable():
     vsort = Derivative._sort_variable_count
-
+    def vsort0(*v, **kw):
+        reverse = kw.get('reverse', False)
+        return [i[0] for i in vsort([(i, 0) for i in (
+            reversed(v) if reverse else v)])]
+
+    for R in range(2):
+        assert vsort0(y, x, reverse=R) == [x, y]
+        assert vsort0(f(x), x, reverse=R) == [x, f(x)]
+        assert vsort0(f(y), f(x), reverse=R) == [f(x), f(y)]
+        assert vsort0(g(x), f(y), reverse=R) == [f(y), g(x)]
+        assert vsort0(f(x, y), f(x), reverse=R) == [f(x), f(x, y)]
+        fx = f(x).diff(x)
+        assert vsort0(fx, y, reverse=R) == [y, fx]
+        fy = f(y).diff(y)
+        assert vsort0(fy, fx, reverse=R) == [fx, fy]
+        fxx = fx.diff(x)
+        assert vsort0(fxx, fx, reverse=R) == [fx, fxx]
+        assert vsort0(Basic(x), f(x), reverse=R) == [f(x), Basic(x)]
+        assert vsort0(Basic(y), Basic(x), reverse=R) == [Basic(x), Basic(y)]
+        assert vsort0(Basic(y, z), Basic(x), reverse=R) == [
+            Basic(x), Basic(y, z)]
+        assert vsort0(fx, x, reverse=R) == [
+            x, fx] if R else [fx, x]
+        assert vsort0(Basic(x), x, reverse=R) == [
+            x, Basic(x)] if R else [Basic(x), x]
+        assert vsort0(Basic(f(x)), f(x), reverse=R) == [
+            f(x), Basic(f(x))] if R else [Basic(f(x)), f(x)]
+        assert vsort0(Basic(x, z), Basic(x), reverse=R) == [
+            Basic(x), Basic(x, z)] if R else [Basic(x, z), Basic(x)]
+    assert vsort([]) == []
+    assert _aresame(vsort([(x, 1)]), [Tuple(x, 1)])
+    assert vsort([(x, y), (x, z)]) == [(x, y + z)]
+    assert vsort([(y, 1), (x, 1 + y)]) == [(x, 1 + y), (y, 1)]
+    # coverage complete; legacy tests below
     assert vsort([(x, 3), (y, 2), (z, 1)]) == [(x, 3), (y, 2), (z, 1)]
-
-    assert vsort([(h(x), 1), (g(x), 1), (f(x), 1)]) == [(f(x), 1), (g(x), 1), (h(x), 1)]
-
-    assert vsort([(z, 1), (y, 2), (x, 3), (h(x), 1), (g(x), 1), (f(x), 1)]) == [(x, 3), (y, 2), (z, 1), (f(x), 1), (g(x), 1), (h(x), 1)]
-
-    assert vsort([(x, 1), (f(x), 1), (y, 1), (f(y), 1)]) == [(x, 1), (f(x), 1), (y, 1), (f(y), 1)]
-
-    assert vsort([(y, 1), (x, 2), (g(x), 1), (f(x), 1), (z, 1), (h(x), 1), (y, 2), (x, 1)]) == [(x, 2), (y, 1), (f(x), 1), (g(x), 1), (z, 1), (h(x), 1), (x, 1), (y, 2)]
-
-    assert vsort([(z, 1), (y, 1), (f(x), 1), (x, 1), (f(x), 1), (g(x), 1)]) == [(y, 1), (z, 1), (f(x), 1), (x, 1), (f(x), 1), (g(x), 1)]
-
-    assert vsort([(z, 1), (y, 2), (f(x), 1), (x, 2), (f(x), 2), (g(x), 1),
-    (z, 2), (z, 1), (y, 1), (x, 1)]) == [(y, 2), (z, 1), (f(x), 1), (x, 2), (f(x), 2), (g(x), 1), (x, 1), (y, 1), (z, 2), (z, 1)]
-
-
-    assert vsort(((y, 2), (x, 1), (y, 1), (x, 1))) == [(x, 1), (x, 1), (y, 2), (y, 1)]
-
+    assert vsort([(h(x), 1), (g(x), 1), (f(x), 1)]) == [
+        (f(x), 1), (g(x), 1), (h(x), 1)]
+    assert vsort([(z, 1), (y, 2), (x, 3), (h(x), 1), (g(x), 1),
+        (f(x), 1)]) == [(x, 3), (y, 2), (z, 1), (f(x), 1), (g(x), 1),
+        (h(x), 1)]
+    assert vsort([(x, 1), (f(x), 1), (y, 1), (f(y), 1)]) == [(x, 1),
+        (y, 1), (f(x), 1), (f(y), 1)]
+    assert vsort([(y, 1), (x, 2), (g(x), 1), (f(x), 1), (z, 1),
+        (h(x), 1), (y, 2), (x, 1)]) == [(x, 3), (y, 3), (z, 1),
+        (f(x), 1), (g(x), 1), (h(x), 1)]
+    assert vsort([(z, 1), (y, 1), (f(x), 1), (x, 1), (f(x), 1),
+        (g(x), 1)]) == [(x, 1), (y, 1), (z, 1), (f(x), 2), (g(x), 1)]
+    assert vsort([(z, 1), (y, 2), (f(x), 1), (x, 2), (f(x), 2),
+        (g(x), 1), (z, 2), (z, 1), (y, 1), (x, 1)]) == [(x, 3), (y, 3),
+        (z, 4), (f(x), 3), (g(x), 1)]
+    assert vsort(((y, 2), (x, 1), (y, 1), (x, 1))) == [(x, 2), (y, 3)]
     assert isinstance(vsort([(x, 3), (y, 2), (z, 1)])[0], Tuple)
+    assert vsort([(x, 1), (f(x), 1), (x, 1)]) == [(x, 2), (f(x), 1)]
+    assert vsort([(y, 2), (x, 3), (z, 1)]) == [(x, 3), (y, 2), (z, 1)]
+    assert vsort([(h(y), 1), (g(x), 1), (f(x), 1)]) == [
+        (f(x), 1), (g(x), 1), (h(y), 1)]
+    assert vsort([(x, 1), (y, 1), (x, 1)]) == [(x, 2), (y, 1)]
+    assert vsort([(f(x), 1), (f(y), 1), (f(x), 1)]) == [
+        (f(x), 2), (f(y), 1)]
+    dfx = f(x).diff(x)
+    self = [(dfx, 1), (x, 1)]
+    assert vsort(self) == self
+    assert vsort([
+        (dfx, 1), (y, 1), (f(x), 1), (x, 1), (f(y), 1), (x, 1)]) == [
+        (y, 1), (f(x), 1), (f(y), 1), (dfx, 1), (x, 2)]
+    dfy = f(y).diff(y)
+    assert vsort([(dfy, 1), (dfx, 1)]) == [(dfx, 1), (dfy, 1)]
+    d2fx = dfx.diff(x)
+    assert vsort([(d2fx, 1), (dfx, 1)]) == [(dfx, 1), (d2fx, 1)]
+
 
 def test_multiple_derivative():
     # Issue #15007
-    assert f(x,y).diff(y,y,x,y,x) == Derivative(f(x, y), (x, 2), (y, 3))
+    assert f(x, y).diff(y, y, x, y, x
+        ) == Derivative(f(x, y), (x, 2), (y, 3))
+
 
 def test_unhandled():
     class MyExpr(Expr):
@@ -677,8 +731,8 @@ def _eval_derivative(self, s):
                 return None
 
     expr = MyExpr(x, y, z)
-    assert diff(expr, x, y, f(x), z) == Derivative(expr, f(x), z)
-    assert diff(expr, f(x), x) == Derivative(expr, f(x), x)
+    assert diff(expr, x, y, f(x), z) == Derivative(expr, z, f(x))
+    assert diff(expr, f(x), x) == Derivative(expr, x, f(x))
 
 
 def test_nfloat():
@@ -998,3 +1052,19 @@ def test_undefined_function_eval():
     assert sympify(expr) == expr
     assert type(sympify(expr)).fdiff.__name__ == "<lambda>"
     assert expr.diff(t) == cos(t)
+
+
+def test_issue_15241():
+    F = f(x)
+    Fx = F.diff(x)
+    assert (F + x*Fx).diff(x, Fx) == 2
+    assert (F + x*Fx).diff(Fx, x) == 1
+    assert (x*F + x*Fx*F).diff(F, x) == x*Fx.diff(x) + Fx + 1
+    assert (x*F + x*Fx*F).diff(x, F) == x*Fx.diff(x) + Fx + 1
+    y = f(x)
+    G = f(y)
+    Gy = G.diff(y)
+    assert (G + y*Gy).diff(y, Gy) == 2
+    assert (G + y*Gy).diff(Gy, y) == 1
+    assert (y*G + y*Gy*G).diff(G, y) == y*Gy.diff(y) + Gy + 1
+    assert (y*G + y*Gy*G).diff(y, G) == y*Gy.diff(y) + Gy + 1


Code review comments:
diff_hunk: @@ -1298,72 +1298,101 @@ def _remove_derived_once(cls, v):
         return [i[0] if i[1] == 1 else i for i in v]
 
     @classmethod
-    def _sort_variable_count(cls, varcounts):
+    def _sort_variable_count(cls, vc):
         """
-        Sort (variable, count) pairs by variable, but disallow sorting of non-symbols.
+        Sort (variable, count) pairs into canonical order while
+        retaining order of variables that do not commute during
+        differentiation:
 
-        The count is not sorted. It is kept in the same order as the input
-        after sorting by variable.
-
-        When taking derivatives, the following rules usually hold:
-
-        * Derivative wrt different symbols commute.
-        * Derivative wrt different non-symbols commute.
-        * Derivatives wrt symbols and non-symbols don't commute.
+        * symbols and functions commute with each other
+        * derivatives commute with each other
+        * a derivative doesn't commute with anything it contains
+        * any other object is not allowed to commute if it has
+          free symbols in common with another object
 
         Examples
         ========
 
-        >>> from sympy import Derivative, Function, symbols
+        >>> from sympy import Derivative, Function, symbols, cos
         >>> vsort = Derivative._sort_variable_count
         >>> x, y, z = symbols('x y z')
         >>> f, g, h = symbols('f g h', cls=Function)
 
-        >>> vsort([(x, 3), (y, 2), (z, 1)])
-        [(x, 3), (y, 2), (z, 1)]
-
-        >>> vsort([(h(x), 1), (g(x), 1), (f(x), 1)])
-        [(f(x), 1), (g(x), 1), (h(x), 1)]
+        Contiguous items are collapsed into one pair:
 
-        >>> vsort([(z, 1), (y, 2), (x, 3), (h(x), 1), (g(x), 1), (f(x), 1)])
-        [(x, 3), (y, 2), (z, 1), (f(x), 1), (g(x), 1), (h(x), 1)]
+        >>> vsort([(x, 1), (x, 1)])
+        [(x, 2)]
+        >>> vsort([(y, 1), (f(x), 1), (y, 1), (f(x), 1)])
+        [(y, 2), (f(x), 2)]
 
-        >>> vsort([(x, 1), (f(x), 1), (y, 1), (f(y), 1)])
-        [(x, 1), (f(x), 1), (y, 1), (f(y), 1)]
+        Ordering is canonical.
 
-        >>> vsort([(y, 1), (x, 2), (g(x), 1), (f(x), 1), (z, 1), (h(x), 1), (y, 2), (x, 1)])
-        [(x, 2), (y, 1), (f(x), 1), (g(x), 1), (z, 1), (h(x), 1), (x, 1), (y, 2)]
+        >>> def vsort0(*v):
+        ...     # docstring helper to
+        ...     # change vi -> (vi, 0), sort, and return vi vals
+        ...     return [i[0] for i in vsort([(i, 0) for i in v])]
 
-        >>> vsort([(z, 1), (y, 1), (f(x), 1), (x, 1), (f(x), 1), (g(x), 1)])
-        [(y, 1), (z, 1), (f(x), 1), (x, 1), (f(x), 1), (g(x), 1)]
+        >>> vsort0(y, x)
+        [x, y]
+        >>> vsort0(g(y), g(x), f(y))
+        [f(y), g(x), g(y)]
 
-        >>> vsort([(z, 1), (y, 2), (f(x), 1), (x, 2), (f(x), 2), (g(x), 1), (z, 2), (z, 1), (y, 1), (x, 1)])
-        [(y, 2), (z, 1), (f(x), 1), (x, 2), (f(x), 2), (g(x), 1), (x, 1), (y, 1), (z, 2), (z, 1)]
+        Symbols are sorted as far to the left as possible but never
+        move to the left of a derivative having the same symbol in
+        its variables; the same applies to AppliedUndef which are
+        always sorted after Symbols:
 
+        >>> dfx = f(x).diff(x)
+        >>> assert vsort0(dfx, y) == [y, dfx]
+        >>> assert vsort0(dfx, x) == [dfx, x]
         """
-        sorted_vars = []
-        symbol_part = []
-        non_symbol_part = []
-        for (v, c) in varcounts:
-            if not v.is_symbol:
-                if len(symbol_part) > 0:
-                    sorted_vars.extend(sorted(symbol_part,
-                                              key=lambda i: default_sort_key(i[0])))
-                    symbol_part = []
-                non_symbol_part.append((v, c))
+        from sympy.utilities.iterables import uniq, topological_sort
+        if not vc:
+            return []
+        vc = list(vc)
+        if len(vc) == 1:
+            return [Tuple(*vc[0])]
+        V = list(range(len(vc)))
+        E = []
+        v = lambda i: vc[i][0]
+        D = Dummy()
+        def _block(d, v, wrt=False):
+            # return True if v should not come before d else False
+            if d == v:
+                return wrt
+            if d.is_Symbol:
+                return False
+            if isinstance(d, Derivative):
+                # a derivative blocks if any of it's variables contain
+                # v; the wrt flag will return True for an exact match
+                # and will cause an AppliedUndef to block if v is in
+                # the arguments
+                if any(_block(k, v, wrt=True)
+                        for k in [vi[0] for vi in d.variable_count]):
path: sympy/core/function.py
text: @user1:
Could this be `for k, _ in d.variable_count`?

@author:
Yes. In the final version of my work this will be `for k in d._wrt_variables`. I guess the first thing that came to mind was to replace the `d._wrt_variables` with the equivalent list comprehension rather than rewriting...and I never looked back. :-)