3

I am well aware that if you have a class method that uses the enum's class name for type hinting, there is a hack to get it to work for Python 3.6 and below.

Instead of...

class Release(Enum):
   ...
   @classmethod
   def get(cls, release: Release):
      ...

You need to use the string value like so...

class Release(Enum):
   ...
   @classmethod
   def get(cls, release: "Release"):
      ...

I believe in Python 3.7 and above there is a pythonic way around this "hack" where you don't have to use quotes. The reason is something along the lines of "the class doesn't exist yet until all the methods and varibles are done first". Since the class doesn't exist yet, I can't use the class name yet, and have to use the quoted string as a hack.

However, I am trying to go one step further and use a default value. And that doesn't work. Is there a pythonic approach for Python 3.6 that isn't a hack? Also, is there a fix in python 3.7 and above?

Code

from enum import Enum

class Release(Enum):
    Canary = (1, [])
    Beta = (2, [1])
    RC = (3, [2, 1])
    Stable = (4, [3, 2, 1])

    def __new__(cls, value, cascade):
        obj = object.__new__(cls)
        obj._value_ = value
        obj.current = ["Release" * value] # This would technically be a list of all releasese in this enum. This is just to emulate different values
        obj.cascade = cascade
        return obj

    @classmethod
    def get_all_releases(cls, release: "Release" = Canary):  # Default Value = Release.Canary
        return release.current


print(Release.get_all_releases(Release.Canary))
print(Release.get_all_releases(Release.Beta))
print(Release.get_all_releases(Release.RC))
print(Release.get_all_releases(Release.Stable))

# Error. Even with default value
# print(Release.get_all_releases())

With this code I get the following error message

AttributeError: 'tuple' object has no attribute 'current'

That is because it returns the tuple of Canary instead of the actual value.

Christopher Rucinski
  • 4,737
  • 2
  • 27
  • 58

3 Answers3

1

While it's definitely a workaround, this seemed to work well for me:

@classmethod
def get_all_releases(cls, release: "Release" = Canary):  # Default Value = Release.Canary
    if release == (Release.Canary.value,):
        return Release.Canary.current
    return release.current

It does work for whatever value you assign to Canary. So as long as that is your default I believe it will work.


To be more general so that you'd only have to adjust the default in the class definition instead of each function, you could do it as follows:

class Release(Enum):
    Canary = 6,
    Beta = 2,
    RC = 3,
    Stable = 4
    default = Canary

    ...

    @classmethod
    def get_all_releases(cls, release: "Release" = default):
        if release == (Release.Canary.value,):
            return Release.Canary.current
        return release.current
ufoxDan
  • 609
  • 4
  • 13
  • As far as you are aware, there isn't any built-in way of doing this, correct? – Christopher Rucinski Nov 07 '19 at 08:13
  • I don't know of one and couldn't find anything with lesser complexity, but it's not something I've tried before so I can't be 100% certain. – ufoxDan Nov 07 '19 at 08:17
  • I updated the answer to show a more general way of setting and using the default. There are also some other answers here https://stackoverflow.com/questions/44867597/is-there-a-way-to-specify-a-default-value-for-python-enums but if this method works for you it at least doesn't require too many changes. – ufoxDan Nov 07 '19 at 15:10
  • You're missing a comma on `Stable`. – Ethan Furman Nov 30 '21 at 21:19
1

There are a couple things you can do in your Release Enum to make life easier, the first being a technique shown here:

    def __new__(cls, value, cascade):
        obj = object.__new__(cls)
        obj._value_ = value
        obj.current = ["Release" * value]      # not sure what this should actually be

        # if always the previous versions (don't need cascade defined)
        obj.cascade = sorted(list(cls), reverse=True)

        # if some already defined subset (need cascade defined)
        obj.cascade = [cls._value2member_map_(c) for c in cascade]

        return obj

The second technique can go two ways -- your default is always the first Enum member:

    @classmethod
    def get_all_releases(cls):
        return list(cls[0]).current

or, if the default could be any member, then something similar to this answer should work:

class add_default:
    """
    add DEFAULT psuedo-member to enumeration; use first member if none specified
    (default should be name of member)
    """
    def __init__(self, default=''):
        self._default = default
    def __call__(self, enumeration):
        if self._default:
            member = enumeration[self._default]
        else:
            member = enumeration[enumeration._member_names_[0]]
        enumeration._member_map_['DEFAULT'] = member
        return enumeration

Your final Enum would then look like (assuming cascade is all previous members and using the decorator approach):

@add_default('Canary')
class Release(Enum):
    Canary = 1
    Beta = 2
    RC = 3
    Stable = 4
    def __new__(cls, value):
        obj = object.__new__(cls)
        obj._value_ = value
        obj.current = ["Release" * value]      # not sure what this should actually be or how it's calculated
        obj.cascade = list(cls)[::-1]
        return obj
    @classmethod
    def get_all_releases(cls, release: "Release" = None):
        if release is None:
            release = cls.DEFAULT
        return release.current

and in use:

>>> Release.DEFAULT
<Release.Canary: 1>

>>> Release.get_all_releases()
['Release']

>>> Release.get_all_releases(Release.RC)
['ReleaseReleaseRelease']

Original Answer

The problem you are having with your code is here:

class Release(Enum):
    Canary = 1,

By including that extra comma you have made the value for Canary be (1, ). Remove that comma to get rid of the tuple exception.

Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
  • Thanks for explaining why the value was `(1, )`. I didn't fully understand that until now; however, my real code **does** use a tuple with the second element being a list. I tried to make a minimal viable example – Christopher Rucinski Nov 07 '19 at 15:27
  • The problem is that it gives an `AttributeError` with `int` instead of `tuple` – Christopher Rucinski Nov 07 '19 at 17:24
  • @ChristopherRucinski: Updated answer to match your updated question. :) – Ethan Furman Nov 07 '19 at 17:32
  • @ChristopherRucinski: Argh. I should never post code without running it first! It works now. :/ – Ethan Furman Nov 07 '19 at 18:16
  • A lot of good information to parse through here. In this question I was just trying to figure out a pythonic way of using a default Enum value in the classmethod, so I left out a lot important detail that you had to assume. For example, `current` is actually a `dict` with the keys being `latest`, and each version such as `1.0`, `1.1`, `1.2`, `1.3`, `2.0` `2.1`, `2.2`, `3.0`, etc... and the values being some data like a download link. Also, while `cascade` usually is all previous versions, there is one weird case that I didn't include where it doesn't follow that logic. – Christopher Rucinski Nov 08 '19 at 14:44
1

Took a hint from @ufoxDan with his answer, but tried to make it less workaround-y and more natural.

Basically, I started by checking the type(release) before returning and noticed that I got the results of..

<enum 'Release'>
<enum 'Release'>
<enum 'Release'>
<enum 'Release'>
<class 'tuple'>

I noticed that if the type was Release then I can just execute the code, however if it was anything else, like None instead of the uncreated Canary type, then I could assume it was asking for Canary. So I did the following...

@classmethod
def get_all_releases(cls, release: "Release" = None):
   if type(release) is Release:
       return release.current
   return Release.Canary.current

# Now these all work
print(Release.get_all_releases())
print(Release.get_all_releases(Release.Canary))
print(Release.get_all_releases(Release.Stable))

This appears to be the most pythonic way of achieving the results. This seems to also be the best way while reading the code and without repeating code. Anyone should be able to implement something similar it seems.

Christopher Rucinski
  • 4,737
  • 2
  • 27
  • 58
  • I like it. Just note that if you ever need to change which value is the default you will need to refactor it in every function that makes use of this default (not bad for one function though). So you can also set `default = Canary` in the enum definition, and then do `return Release.default.current`. Not a major issue but it can ease future development if its a big project :) – ufoxDan Nov 07 '19 at 16:27
  • 1
    @ufoxDan I understand what you mean. My code currently loops through all enumerations in other places and I think I would have to change that code to deal with the new `default` enum. I will look into that if I need to update the class more – Christopher Rucinski Nov 07 '19 at 17:21