pasztorpisti / py-flags

Type-safe (bit)flags for python 3
MIT License
36 stars 5 forks source link

Support for `enum.auto()` #13

Open autumnjolitz opened 1 month ago

autumnjolitz commented 1 month ago

I found that using the auto() call from the enum stdlib package was much less verbose and cleaner to the eye than flags.UNDEFINED

Given that I have a types.py module in my project that declares enhanced base types, I was able to neatly override flags such that it was able to swap out auto() for a flags.UNDEFINED at the definition of a type.

Here's how i did it:

from flags import Flags, UNDEFINED as Flags_Undefined
from collections.abc import (Buffer, Iterable)
from enum import auto

class BaseFlags(Flags):
    @classmethod
    def flag_attribute_value_to_bits_and_data(cls, name, value):
        data = Flags_Undefined
        if isinstance(value, (Buffer, bytes, str)):
            data = value
            value = Flags_Undefined
        elif isinstance(value, Iterable):
            value, *data = value
            if not data:
                data = Flags_Undefined
            elif len(data) == 1:
                (data,) = data
        if isinstance(value, auto):
            value = Flags_Undefined
        if data is not Flags_Undefined:
            return Flags.flag_attribute_value_to_bits_and_data(name, (value, data))
        return Flags.flag_attribute_value_to_bits_and_data(name, value)

class TestFlags(BaseFlags):
    """
    >>> TestFlags.C
    <TestFlags.C bits=0x0004 data='my data'>
    >>> TestFlags.A  | TestFlags.C
    <TestFlags(A|C) bits=0x0005>
    >>>
    """
    A = auto()
    B = auto()
    C = auto(), "my data"

As you can see, it's relatively approachable to support instances of enum.auto() and may be a nice quality of life enhancement.

In addition I discovered it's rather nice to be able to support additional data items by gathering up all values past said auto()

I've used this package for years. :)

pasztorpisti commented 1 month ago

Thank you for taking the time to raise this issue.

Unfortunately this feature of the py-flags package (auto-generated values and associated data) isn't well designed, it gives too many options in a messy unorganised way. If I decided to break backwards compatibility in a version 2.0.0, I'd replace this design completely to something much simpler and stricter (allowing only enum_member = value or enum_member = value, data as the default behaviour with auto as a possible value).

I found that using the auto() call from the enum stdlib package was much less verbose and cleaner to the eye than flags.UNDEFINED

What about the following hacks? :smirk:

def auto():
    return flags.UNDEFINED

# OR

# Even more concise! However, "constants" in python are usually in uppercase...
auto = flags.UNDEFINED

Supporting something like the above (or a more specific/unique auto object, like class auto: pass) in the flags package isn't a bad idea, and can be added in a backward compatible way.

However, these hacks wouldn't handle your special cases (like isinstance(value, (Buffer, bytes, str))) that seem to be modifications that are specific to your application. To get that behaviour/syntax you would still need the flag_attribute_value_to_bits_and_data() override in your code.

Adding support for auto is a relatively small change but it triggers a cascade of tasks:

Unfortunately, I have to prepare for interviews, so I can't promise anything in the coming weeks.

In addition I discovered it's rather nice to be able to support additional data items by gathering up all values past said auto()

I guess you mean something like option 1 below:

class MyFlags(Flags):
    # option 1: data = ('data_item_1', 'data_item_2')
    member1 = auto(), 'data_item_1', 'data_item_2'

    # option 2: data = ('data_item_1', 'data_item_2')
    member2 = auto(), ('data_item_1', 'data_item_2')

Option 1 may not be a good idea. It gives the user too many options to solve the same problem (which isn't pythonic in general), just like some of my over-complicated Frankenstein designs in the past. Another unintuitive aspect of this design is that the first data item ('data_item_1' in our example) can behave in two different ways (direct value or just an item in a tuple) depending on whether there is only one data item or more. Behaviour like this can already be added with an application-specific flag_attribute_value_to_bits_and_data() override but in case of library-default behaviour I'd stick to a simpler intuitive implementation.

Option 2 requires typing two more characters but at the same time it makes the intention very clear. There is always only one data item (the tuple in our example) and its value is exactly what the reader can see in the code, it's intuitive. Code is generally written much less often than read so typing a few more characters to make things clearer isn't necessarily bad. Also, this option allows the user to specify a different type of container if needed.

I've used this package for years. :)

I'm glad you enjoy using this package! :-)

However, as you probably know, python 3.6 introduced enum.Flag and enum.IntFlag about a year after this package was released, so considering those when they meet the needs of your project might be a good idea.