Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FIX: make set_text(None) keep string empty instead of "None" #10392

Merged
merged 4 commits into from Feb 14, 2018

Conversation

jklymak
Copy link
Member

@jklymak jklymak commented Feb 7, 2018

PR Summary

As documented in #10391, legend.set_title() doesn't do sensible things with the default title=None (i.e it set the string of the legend title to the string "None").

This is kind of an API change in that if anyone was checking for the string "None" to see if the legend title was empty will now fail, but lets hope no one was doing that.

As pointed out below by @anntzer the problem is more wide spread: The updated PR now checks if s is None in Text.set_text and if it is does not do anything. Before it would set the string to "None" which is silly.

One could go further and insist that s is a string (so the user knows what they are doing), but that'll likely break lots of people assuming the string cast happens.

This still needs a test....

PR Checklist

  • Has Pytest style unit tests
  • Code is PEP 8 compliant
  • API change noted

@anntzer
Copy link
Contributor

anntzer commented Feb 7, 2018

There's a bunch of places where passing in None is interpreted as the string "None" (e.g. xlabel, title) and I would rather have them all fixed at once rather than in a piecemeal fashion.

@jklymak
Copy link
Member Author

jklymak commented Feb 7, 2018

Yeah, OK, fair enough.

I'd propse in text.py:

def set_text(self, s):
        """
        Set the text string *s*

        It may contain newlines (``\\n``) or math in LaTeX syntax.

        ACCEPTS: string or anything printable with '%s' conversion.
        """
        if s is not None:
            self._text = '%s' % (s,)
            self.stale = True

@jklymak jklymak changed the title FIX: make legend title state more consistent if empty FIX: make set_text(None) keep string empty instead of "None" Feb 7, 2018
@anntzer
Copy link
Contributor

anntzer commented Feb 7, 2018

probably an API change note

"""
self._text = '%s' % (s,)
self.stale = True
if s is not None:
Copy link
Contributor

@anntzer anntzer Feb 7, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't you always clear the text (and mark as stale) when s is None?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, good question! Open to suggestions

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set to empty string if None is passed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot to say here:

  • IMO '%s' % (s,) is an anti-pattern. It is identical to str(s).
  • self._text is not defined in __init__. It only calls set_text(text). Something like Text(text=None).get_text() would fail with an attibute error in your code. It's good practice to declare your attributes in init. You should add self._text = '' to init.
  • I would expect that set_text(None) and set_text('') are completely equivalent.
  • Minor thing: Add a colon at the end of the sentence in the docstring.

So either always stale:

def set_text(s):
    self._text = str(s) if s is not None else ''
    self.stale = True

or stale only when there's really a change:

def set_text(s):
    if s is None:
        s = ''
    if s != self._text:
        self._text = s
        self.stale = True

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch. You're right I didn't notice that self._text wasn't being set.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@timhoffm not sure where you want the colon...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, full stop: "Set the text string s."

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great. Fixed. Also, your logic was cleaner, so used that (second option).

fig, ax = plt.subplots()
ax.plot(range(10))
leg = ax.legend()
assert leg.get_title().get_text() == ""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert not leg.get_title().get_text()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should rely on boolean-ness here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is PEP-8 convention

For sequences, (strings, lists, tuples), use the fact that empty sequences are false.

But I'm ok, if you want to have an explicit comparison.


It may contain newlines (``\\n``) or math in LaTeX syntax.

ACCEPTS: string or anything printable with '%s' conversion.
ACCEPTS: string or anything printable with '%s' conversion, except
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"anything printable with '%s' conversion" is technically correct, but feels a bit beside the point. This has nothing to do with printing. I don't really know a good alternative.

According to __str__ docs "anything with an "informal" string representation" would be right, but I'm afraid that users will not understand this.

Maybe "anything that can be formatted by '%s'" or "anything that can be converted by '%s'"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually any object has a __str__... (yes, you can technically make __str__ raise an exception but whatever).
The test should more be something like "any object, will be cast to a string using str" (don't care much for Py2 now especially if this goes to mpl3.0...).


It may contain newlines (``\\n``) or math in LaTeX syntax.

ACCEPTS: string or anything printable with '%s' conversion.
ACCEPTS: string or anything printable with '%s' conversion, except
``None``, which leaves the text string empty:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which is equivalent to an empty string.

@jklymak
Copy link
Member Author

jklymak commented Feb 7, 2018

@timhoffm python 2.7 gave unicode errors for str(s) instead of '%s' % s. Since I'd rather gouge my eyes out than mess around with py2/3 differences in unicode support, I reverted that suggestion. If it drives you nuts, a) feel free to suggest a fix or b) keep it in mind for 3.0.

@jklymak jklymak force-pushed the fix-better-legend-title-state branch from 53e8eec to bb9a330 Compare February 7, 2018 23:55
@tacaswell tacaswell added this to the v3.0 milestone Feb 8, 2018
@timhoffm
Copy link
Member

timhoffm commented Feb 8, 2018

@jklymak Ok, leaving the str(s) to py3 only then.

@jklymak
Copy link
Member Author

jklymak commented Feb 8, 2018

@tacaswell so for 3.0 milestones PRs can we ignore failing 2.7 tests? If so, then I can implement @timhoffm's str(s) suggestion.

@timhoffm
Copy link
Member

timhoffm commented Feb 8, 2018

Approved, whatever the decision on str(s) will be.

@jklymak jklymak force-pushed the fix-better-legend-title-state branch from d824411 to 7bbf4e5 Compare February 8, 2018 16:58
@jklymak
Copy link
Member Author

jklymak commented Feb 8, 2018

Thanks for your help @timhoffm

@jklymak
Copy link
Member Author

jklymak commented Feb 8, 2018

OK, have gone back to the cast if this is milestones 3.0. Should pass the 3.x tests. Modified the docstring as per @anntzer comment above.

Copy link
Member

@phobson phobson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great. I'll merge pending successful CI

@jklymak
Copy link
Member Author

jklymak commented Feb 8, 2018

Just to be clear, 2.7 tests/doc build will fail. But if this is 3.0 those tests should be ignored, right @tacaswell?

@phobson
Copy link
Member

phobson commented Feb 8, 2018

Circle CI failure seems unrelated:

Encoding error:
'ascii' codec can't encode character u'\u2212' in position 0: ordinal not in range(128)
The full traceback has been saved in /tmp/sphinx-err-y7dr1d.log, if you want to report the issue to the developers.
Makefile:20: recipe for target 'html' failed
make: *** [html] Error 1
Exited with code 2

u'\u2212' is the "minus sign"

@jklymak
Copy link
Member Author

jklymak commented Feb 8, 2018

it’s definitley because set_text calls str(s) now. If I take that out the docs build.

@story645
Copy link
Member

story645 commented Feb 8, 2018

@jklymak technically cat.to_str(value) was basically designed to iron out Py2/Py3 string differences particularly for labeling (I only call it in the Formmater). It's messy, and should probably be in cbook, but it'd possibly work.

@jklymak
Copy link
Member Author

jklymak commented Feb 8, 2018

Well, I'll just leave it the way it was before I touched anything.... We can revisit for python 3...

@efiring efiring merged commit 84292c4 into matplotlib:master Feb 14, 2018
@efiring
Copy link
Member

efiring commented Feb 14, 2018

This seemed to be ready to go, so I used "squash and merge". Let's see if that broke anything.

@RpiController
Copy link

Could someone also check to make sure that ax.get_legend().get_visible() is working properly?
It is always returning True for me.

@jklymak
Copy link
Member Author

jklymak commented Apr 3, 2018

What does working properly mean in this context? How do you expect it to behave and why?

@RpiController
Copy link

After executing ax.get_legend().set_visible(False) I would like ax.get_legend().get_visible() to return False.
That seems like it would be proper to me. Currently it is always returning True no matter what.

Another issue that I think needs to be fixed is that after setting ax.get_legend().set_visible(False) and then back True again, all the specifics of my legend are lost (legend position, marker size, etc)

@jklymak
Copy link
Member Author

jklymak commented Apr 3, 2018

Can you provide a MSCE that has this problem? I can't reproduce. If you do provide an example, please open a new issue...

If I do:

import matplotlib.pyplot as plt

fig, ax = plt.subplots()

ax.plot(range(10), label='Boo')
leg = ax.legend()
print(leg)
ax.get_legend().set_visible(False)
print(ax.get_legend().get_visible())
ax.get_legend().set_visible(True)
print(ax.get_legend().get_visible())
plt.show()

I get

Legend
False
True

and the legend appears as expected... If I don't set_visible(True) it remains hidden.

@RpiController
Copy link

I'm not using plt since my plot is embedded in Tkinter. My code is pretty complicated, it's python 2.7 on a RaspberryPi with matplotlib 2.1. I was hoping someone else would have run into the same issue and found an easy fix or workaround. If not, I will just live with it.

The other issue of losing the legend parameters between making visible and invisible seems like a more serious flaw but I don't really want to go through the hassle of simplifying my code to provide a MSCE and doing whatever is needed to open a new issue. I'm kind of a newb on here.

This is just my good deed of bringing it to someone's attention.

@jklymak
Copy link
Member Author

jklymak commented Apr 3, 2018

The other issue of losing the legend parameters between making visible and invisible seems like a more serious flaw but I don't really want to go through the hassle of simplifying my code to provide a MSCE and doing whatever is needed to open a new issue. I'm kind of a newb on here.

The example above doesn't have that behaviour, so its pretty hard to chase down what the issue might be that you are having. If you figure it out, let us know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

9 participants