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

bpo-40066: Enum: modify repr() and str() #22392

Merged
merged 21 commits into from
Mar 31, 2021

Conversation

ethanfurman
Copy link
Member

@ethanfurman ethanfurman commented Sep 24, 2020

Enumerations created from Enum._convert_ will have their repr() changed to be

module.member_name

and their str() to be

member_name

The exception is StrEnum, whose str() will be

value

as in, the member's value.

https://bugs.python.org/issue40066

Copy link
Member

@gvanrossum gvanrossum left a comment

Choose a reason for hiding this comment

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

Nice. How did you track down all the docs that need to be changed? (I note there aren't any doc changes do to the change in str().)

Lib/enum.py Outdated Show resolved Hide resolved
Lib/re.py Show resolved Hide resolved
Comment on lines +1 to +4
Enum's `repr()` and `str()` have changed: `repr()` is now *EnumClass.MemberName*
and `str()` is *MemberName*. Additionally, stdlib Enum's whose contents are
available as module attributes, such as `RegexFlag.IGNORECASE`, have their
`repr()` as *module.name*, e.g. `re.IGNORECASE`.
Copy link
Member

Choose a reason for hiding this comment

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

Maybe also add a section to what's new in 3.10?

Copy link
Member Author

Choose a reason for hiding this comment

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

I thought blurb did that?

Copy link
Member

Choose a reason for hiding this comment

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

No, blurb just updates Misc/NEWS, which is an endless list of things (though more focused than the commit log :-).

What's new in 3.10 is a separate doc written by human editor who curate the most important news and group it by affected area.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

Also the versionchanged directive is needed in the module documentation.

@ethanfurman
Copy link
Member Author

Nice. How did you track down all the docs that need to be changed? (I note there aren't any doc changes do to the change in str().)

grep -R "<[A-Za-z\.]*: [^>]*>" *

Assuming no one comes up with a strong reason to not commit this PR, I will need to do some rewriting of enum.rst -- there's currently a good-sized section dealing with valueless Enums and updating the repr() to not show the value, etc.

@gvanrossum
Copy link
Member

grep -R "<[A-Za-z\.]*: [^>]*>" *

That doesn't look for occurrences of the str() though, which doesn't have any funky markup.

Assuming no one comes up with a strong reason to not commit this PR, I will need to do some rewriting of enum.rst -- there's currently a good-sized section dealing with valueless Enums and updating the repr() to not show the value, etc.

Are you planning to do that in the same PR? (I recommend it.)

FWIW you're unlikely to get anyone to tell you not to commit this from GitHub reviewers or bpo readers. Was there a python-ideas thread? Maybe you want to ask Raymond (if he didn't already concur)?

>>> ~Perm.RWX
<Perm.-8: -8>
Perm.-8
Copy link
Member

Choose a reason for hiding this comment

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

In this case I would prefer ~Perm.RWX or Perm(-8). But this may be a different issue.

Copy link
Member Author

Choose a reason for hiding this comment

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

Negative values are no longer a thing -- they are converted to their positive equivalent.


Another important difference between :class:`IntFlag` and :class:`Enum` is that
if no flags are set (the value is 0), its boolean evaluation is :data:`False`::

>>> Perm.R & Perm.X
<Perm.0: 0>
Perm.0
Copy link
Member

Choose a reason for hiding this comment

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

Perm(0) would look better.

Copy link
Member Author

Choose a reason for hiding this comment

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

I went with 0x0 to match other non-Flag values, similarly to the way re.RegexFlag does it.

>>> bool(Perm.R & Perm.X)
False

Because :class:`IntFlag` members are also subclasses of :class:`int` they can
be combined with them::

>>> Perm.X | 8
<Perm.8|X: 9>
Perm.8|Perm.X
Copy link
Member

Choose a reason for hiding this comment

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

And here I would prefer Perm.X|8.

Copy link
Member Author

Choose a reason for hiding this comment

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

It is now Perm.X|0x8

@@ -1198,7 +1198,7 @@ constructor. For example::
... example = '11', 16 # '11' will be interpreted as a hexadecimal
... # number
>>> MyEnum.example
Copy link
Member

Choose a reason for hiding this comment

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

I think that it would more useful to use MyEnum.example.value or int(MyEnum.example) in the example.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I added the .value here and in other places.

Lib/enum.py Outdated
raise ValueError(f'_sunder_ names, such as "{key}", are '
'reserved for future Enum use')
raise ValueError(
'_sunder_ names, such as %r, are reserved for future Enum use'
Copy link
Member

Choose a reason for hiding this comment

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

You can use f-string with !r.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, but I'm not using f-strings anywhere else.

Copy link
Member Author

Choose a reason for hiding this comment

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

At some point I'll switch all the %-interpolations to use f-strings.

Lib/enum.py Outdated
for m in self.__class__:
if value & m._value_:
value &= ~m._value_
members.append('re.%s' % (m._name_, ))
Copy link
Member

Choose a reason for hiding this comment

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

re?

Copy link
Member Author

Choose a reason for hiding this comment

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

Oops. Looks like I need to add a test for that branch of code.

Lib/enum.py Outdated
@@ -946,3 +942,42 @@ def _decompose(flag, value):
# we have the breakdown, don't need the value member itself
members.pop(0)
return members, not_covered

def global_int_repr(self):
return '%s.%s' % (self.__class__.__module__, self.name)
Copy link
Member

Choose a reason for hiding this comment

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

What if add an attribute to the enum class which is set to the class name by default and can be set to the module name for enums whose members are exposed as globals?

    return '%s.%s' % (self.__class__._namespacename, self.name)

It will make easier customization of the repr.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think writing a custom __repr__ is not that difficult.

Lib/enum.py Outdated
value &= ~m._value_
members.append('re.%s' % (m._name_, ))
if value:
members.append(hex(value))
Copy link
Member

Choose a reason for hiding this comment

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

For general code I would add a class method for converting value to string. Depending on the class octal or binary may be better than hexadecimal.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fair point.

Comment on lines +1 to +4
Enum's `repr()` and `str()` have changed: `repr()` is now *EnumClass.MemberName*
and `str()` is *MemberName*. Additionally, stdlib Enum's whose contents are
available as module attributes, such as `RegexFlag.IGNORECASE`, have their
`repr()` as *module.name*, e.g. `re.IGNORECASE`.
Copy link
Member

Choose a reason for hiding this comment

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

Also the versionchanged directive is needed in the module documentation.

Lib/enum.py Outdated
self._name_ = '%s' % '|'.join([
str(m._name_ or m._value_) for m in members
])
return self._name_
Copy link
Member

Choose a reason for hiding this comment

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

Now whichever of __str__ and __repr__ is called first will initialize _name_ for all time. Even if they do the same thing now, they might drift in the future. Should this initialization code be in a common function to make sure it's consistent? One option would be to override name() and do it there.

Copy link
Member Author

Choose a reason for hiding this comment

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

The name is now initialized when the pseudo-member is created.

@gvanrossum
Copy link
Member

Who is this PR waiting for now?

@ethanfurman
Copy link
Member Author

GvR wrote:

Who is this PR waiting for now?

For me to make a few changes, and hopefully for Raymond to chime in. I'll update the bpo issue in case he didn't see my email.

non-StrEnum `__str__` will print the member name
StrEnum `__str__` will print the member value
create enum._stdlib_enum decorator for Enums created the normal way: it
adjusts `__repr__` and `__str__` to be the same as if the Enum had been
created using `_convert_`

update various modules to use the new decorator
if `enum.__str__` is either `Enum.__str__` or `global_enum_str` use the
value's `__format__`
repr() = class.member_name
str() = member_name
rename to `global_enum` and have `global_enum` update the module's
namespace with the members

remove `_stdlib_enum` where the enums were not being exported to the
module's namespace
* rename global_int_repr --> global_enum_repr
* adjust global_flag_repr to print hex number for out-of-range values
* extract function when _generate_next_value_ is a static method
- repr() is now ``enum_class.member_name``
- str() is now ``member_name``
- add HOW-TO section for ``Enum``
- change main documentation to be an API reference
@ethanfurman
Copy link
Member Author

@gvanrossum RE: looking for Enum.__str__ output in docs: I manually checked the module documentation for each one that had enum constants.

@ethanfurman ethanfurman changed the title bpo-40066: Enum global repr() and str() bpo-40066: Enum: modify repr() and str() Mar 31, 2021
@ethanfurman ethanfurman merged commit b775106 into python:master Mar 31, 2021
@ethanfurman ethanfurman deleted the enum-global_repr branch April 15, 2021 14:23
tacaswell added a commit to tacaswell/matplotlib that referenced this pull request May 4, 2021
In Python 3.10 the repr and str representation of Enums changed from

 str: 'ClassName.NAME' -> 'NAME'
 repr: '<ClassName.NAME: value>' -> 'ClassName.NAME'

which is more consistent with what str/repr should do, however this breaks
boilerplate which needs to get the ClassName.NAME version in all versions of
Python. Thus, we locally monkey patch our preferred str representation in
here.

bpo-40066
python/cpython#22392
tacaswell added a commit to tacaswell/matplotlib that referenced this pull request May 4, 2021
In Python 3.10 the repr and str representation of Enums changed from

 str: 'ClassName.NAME' -> 'NAME'
 repr: '<ClassName.NAME: value>' -> 'ClassName.NAME'

which is more consistent with what str/repr should do, however this breaks
boilerplate which needs to get the ClassName.NAME version in all versions of
Python. Thus, we locally monkey patch our preferred str representation in
here.

bpo-40066
python/cpython#22392
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants