Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f8a68b7

Browse files
committed
FIX: Disallow twinx/twiny on Axes3D
Closes #31522. The root cause is that Axes3D inherits a lot of functionality from Axes that does not work for 3D. This is a design flaw we cannot easily fix. With this PR we have at least a reasonable mechanism in place to flag not-supported methods. When we get aware of additional methods, we can easily add them.
1 parent dde0763 commit f8a68b7

4 files changed

Lines changed: 83 additions & 19 deletions

File tree

lib/matplotlib/_api/__init__.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,54 @@ def __repr__(self):
3535
UNSET = _Unset()
3636

3737

38+
class UnsupportedError(RuntimeError):
39+
"""
40+
Raised on inherited methods if the child class does not support the functionality
41+
of the base class.
42+
43+
See `.unsupported_method` for details.
44+
"""
45+
46+
47+
class unsupported_method:
48+
"""
49+
Descriptor that creates a method raising `.UnsupportedError`.
50+
51+
Historically, we have quite a few cases of inheritance hierarchies that do not
52+
fully respect the Liskov Substitution Principle, e.g. Axes and Artist. Some of
53+
the methods of a base class may not be implemented in the child class. In that case,
54+
we override the method in the child class to raise `.UnsupportedError`.
55+
56+
Use in a class body to mark inherited methods as unsupported::
57+
58+
class Axes3D(Axes):
59+
twinx = _api.unsupported_method()
60+
61+
Calling ``Axes3D().twinx()`` will raise
62+
"UnsupportedError: Axes3D does not support 'twinx'."
63+
64+
Parameters
65+
----------
66+
append_message : str
67+
Optional additional text to be appended to the error message.
68+
"""
69+
def __init__(self, *, append_message=None):
70+
self.append_message = append_message
71+
72+
def __set_name__(self, owner, name):
73+
message = f"{owner.__name__} does not support '{name}'."
74+
if self.append_message:
75+
message += ' ' + self.append_message
76+
77+
def method(self, *args, **kwargs):
78+
raise UnsupportedError(message)
79+
80+
method.__name__ = name
81+
method.__qualname__ = f"{owner.__qualname__}.{name}"
82+
method.__module__ = owner.__module__
83+
setattr(owner, name, method)
84+
85+
3886
class classproperty:
3987
"""
4088
Like `property`, but also triggers on access via the class, and it is the

lib/matplotlib/tests/test_api.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,27 @@
1818
T = TypeVar('T')
1919

2020

21+
def test_unsupported_method():
22+
class Base:
23+
def method_1(self):
24+
pass
25+
26+
def method_2(self):
27+
pass
28+
29+
class Child(Base):
30+
method_1 = _api.unsupported_method()
31+
method_2 = _api.unsupported_method(append_message="Sorry!")
32+
33+
child = Child()
34+
with pytest.raises(_api.UnsupportedError,
35+
match=r"^Child does not support 'method_1'\.$"):
36+
child.method_1()
37+
with pytest.raises(_api.UnsupportedError,
38+
match=r"^Child does not support 'method_2'\. Sorry!$"):
39+
child.method_2()
40+
41+
2142
@pytest.mark.parametrize('target,shape_repr,test_shape',
2243
[((None, ), "(N,)", (1, 3)),
2344
((None, 3), "(N, 3)", (1,)),

lib/mpl_toolkits/mplot3d/axes3d.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
"""
3232

3333
from collections import defaultdict
34-
from functools import partialmethod
3534
import itertools
3635
import math
3736
import textwrap
@@ -71,6 +70,11 @@ class Axes3D(Axes):
7170
As a user, you do not instantiate Axes directly, but use Axes creation
7271
methods instead; e.g. from `.pyplot` or `.Figure`:
7372
`~.pyplot.subplots`, `~.pyplot.subplot_mosaic` or `.Figure.add_axes`.
73+
74+
.. note::
75+
76+
Some of the inherited behavior of Axes is not applicable to 3d and will raise
77+
an `.UnsupportedError`.
7478
"""
7579
name = '3d'
7680

@@ -1197,16 +1201,16 @@ def set_zscale(self, value, **kwargs):
11971201
"""
11981202
self._set_axis_scale(self.zaxis, value, **kwargs)
11991203

1200-
def _raise_semilog_not_implemented(self, name, *args, **kwargs):
1201-
raise NotImplementedError(
1202-
f"Axes3D does not support {name}. Use ax.set_xscale/set_yscale/set_zscale "
1203-
"and ax.plot(...) instead."
1204-
)
1204+
twinx = _api.unsupported_method()
1205+
twiny = _api.unsupported_method()
1206+
secondary_xaxis = _api.unsupported_method()
1207+
secondary_yaxis = _api.unsupported_method()
12051208

1206-
semilogx = partialmethod(_raise_semilog_not_implemented, "semilogx")
1207-
semilogy = partialmethod(_raise_semilog_not_implemented, "semilogy")
1208-
semilogz = partialmethod(_raise_semilog_not_implemented, "semilogz")
1209-
loglog = partialmethod(_raise_semilog_not_implemented, "loglog")
1209+
_msg = "Use ax.set_xscale/set_yscale/set_zscale and ax.plot(...) instead."
1210+
semilogx = _api.unsupported_method(append_message=_msg)
1211+
semilogy = _api.unsupported_method(append_message=_msg)
1212+
semilogz = _api.unsupported_method(append_message=_msg)
1213+
loglog = _api.unsupported_method(append_message=_msg)
12101214

12111215
get_zticks = _axis_method_wrapper("zaxis", "get_ticklocs")
12121216
set_zticks = _axis_method_wrapper("zaxis", "set_ticks")

lib/mpl_toolkits/mplot3d/tests/test_axes3d.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3164,15 +3164,6 @@ def test_scale3d_autoscale_with_log():
31643164
assert lim[1] > 0, f"{name} upper limit should be positive"
31653165

31663166

3167-
@pytest.mark.parametrize("method", ["semilogx", "semilogy", "semilogz", "loglog"])
3168-
def test_semilog_loglog_not_implemented(method):
3169-
"""semilogx/y/z and loglog should raise NotImplementedError on Axes3D."""
3170-
fig = plt.figure()
3171-
ax = fig.add_subplot(projection='3d')
3172-
with pytest.raises(NotImplementedError, match="Axes3D does not support"):
3173-
getattr(ax, method)([1, 10, 100], [1, 2, 3])
3174-
3175-
31763167
def test_scale3d_calc_coord():
31773168
"""_calc_coord should return data coordinates with correct pane values."""
31783169
fig = plt.figure()

0 commit comments

Comments
 (0)