Skip to main content

No project description provided

Project description

NamespacedEnums

Library for defining containers within Enum Python classes.

Installation

Run

$ pip install namespaced_enums

Using mypy? See mypy support to enable the bundled plugin.

Usage

Library provides two special descriptors for Enum classes:

  • EnumContainer
  • StrictEnumContainer

as well as an additional enum-subclass:

  • NamespacedEnum

EnumContainer

This allows for defining arbitrary containers within Enum classes, e.g.

from enum import Enum
from namespaced_enum import EnumContainer

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3
    
    LIGHT_RED = 4
    LIGHT_GREEN = 5
    LIGHT_BLUE = 6
    
    DARK_RED = 7
    DARK_GREEN = 8
    DARK_BLUE = 9
    
    light_colors = EnumContainer([LIGHT_RED, LIGHT_GREEN, LIGHT_BLUE])
    dark_colors = EnumContainer([DARK_RED, DARK_GREEN, DARK_BLUE])
    
---

>>> print(Color.RED.value in Color.light_colors)  # False
>>> print(Color.DARK_BLUE.value in Color.dark_colors)  # True

# Caution!
# Values within containers are of enum value types
>>> print(Color.dark_colors)  # prints [7, 8, 9] and not [DARK_RED, DARK_GREEN, DARK_BLUE]!

StrictEnumContainer and NamespacedEnum

StrictEnumContainer accepts only dict containers. Using this descriptor within a class inheriting from NamespacedEnum will enforce that the provided dictionary contains all enum possible values:

from namespaced_enums import NamespacedEnum, StrictEnumContainer

class Food(NamespacedEnum):
    spam = "spam"
    eggs = "eggs"
    foo = "foo"

    reactions = StrictEnumContainer(
        {
            spam: "I like it",
            eggs: "I don't like it...",
            foo: "I like?",
        }
    )

---

>>> print(Food.reactions[Food.spam])  # "I like it"

# Caution!
# Unlike the `EnumContainer` this one converts dict keys to enums!
>>> print(list[Food.reactions.keys()])  # prints [<Food.spam: spam>, <Food.eggs: eggs>, <Food.foo: foo>]

Forgetting to provide all possible enum values within a strict container will raise a RuntimeError:

from namespaced_enums import NamespacedEnum, StrictEnumContainer

class Food(NamespacedEnum):
    spam = "spam"
    eggs = "eggs"
    foo = "foo"

    reactions = StrictEnumContainer(
        {
            spam: "I like it",
            eggs: "I don't like it...",
            # missing foo in the dict
        }
    )

---

>>> # Trying to start the program raises a `RuntimeError`:
# The following Food fields do not contain all possible enum values: ['reactions']

mypy support

The library ships with a mypy plugin (and a py.typed marker). Without it, mypy sees container attributes as enum members and rejects indexing them ("Enum" is not indexable).

Enabling the plugin

mypy does not load plugins automatically, so enabling support is two steps:

1. Install the optional dependency (requires mypy >= 1.0):

$ pip install namespaced_enums[mypy]

2. Register the plugin in your mypy configuration. Add the following to whichever config file your project uses:

mypy.ini (or the [mypy] section of setup.cfg):

[mypy]
plugins = namespaced_enums.plugin

pyproject.toml:

[tool.mypy]
plugins = ["namespaced_enums.plugin"]

That's it — no import changes are needed in your own code.

What you get

With the plugin enabled, containers are typed correctly instead of being seen as enum members:

class DogBreed(NamespacedEnum):
    BULLDOG = 'bulldog'
    PUG = 'pug'
    SHIBA = 'shiba'

    size = StrictEnumContainer({BULLDOG: 40, PUG: 30, SHIBA: 35})

reveal_type(DogBreed.size)          # dict[DogBreed, int]
n: int = DogBreed.size[DogBreed.PUG]  # ok, no "not indexable" error

More importantly, the plugin turns the runtime RuntimeError for an incomplete StrictEnumContainer into a static error (error code namespaced-enum), so a forgotten member is caught by mypy instead of at import time:

class DogBreed(NamespacedEnum):
    BULLDOG = 'bulldog'
    PUG = 'pug'
    SHIBA = 'shiba'

    size = StrictEnumContainer({BULLDOG: 40, PUG: 30})
    # error: StrictEnumContainer "size" is missing keys for enum members: SHIBA  [namespaced-enum]

Rationale

It's a common practice to use enums in a project as a way to denote "characteristics" of certain objects.

Consider the following example:

from enum import Enum

class DogBreed(Enum):
    BULLDOG = 'bulldog'
    PUG = 'pug'
    SHIBA = 'shiba'

    
class Dog:
    def __init__(self, breed: DogBreed) -> None:
        self.breed = breed
        
    @property
    def size(self) -> int:
        """Returns size of the dog (in centimeters)."""
        if self.breed == DogBreed.BULLDOG:
            return 40
        elif self.breed == DogBreed.PUG:
            return 30
        elif self.breed == DogBreed.SHIBA:
            return 35
        else:  # pragma: no cover
            raise ValueError(f"Unknown dog breed: {self.breed}")

The Dog.size property implementation poses a threat when it comes to further additions to the DogBreed enum:

  • programmers would have to remember to update it every time they add a new breed support. What if there are more such properties like color, weight or tail length?
  • the if-elif-else chain is hard to test as the last else clause would require injecting some arbitrary value into the DogBreed enum during runtime in order to ensure proper coverage. In my experience, most developers would prefer to add # pragma: no cover instead.
  • the size characteristic should arguably be not part of the Dog class as it is more specific to the DogBreed enum - the Dog class should be aware of how to retrieve that data.

That's why I decided to create a NamespacedEnum class that allows for writing self-contained enums, that can help in maintaining the integrity of the written code. The example above could be rewritten in a following manner:

from namespaced_enums import NamespacedEnum, StrictEnumContainer


class DogBreed(NamespacedEnum):
    BULLDOG = 'bulldog'
    PUG = 'pug'
    SHIBA = 'shiba'

    size = StrictEnumContainer({
        BULLDOG: 40,
        PUG: 30,
        SHIBA: 35,
    })

    
class Dog:
    def __init__(self, breed: DogBreed) -> None:
        self.breed = breed

    @property
    def size(self) -> int:
        """Returns size of the dog (in centimeters)."""
        return DogBreed.size[self.breed]

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

namespaced_enums-0.1.0.tar.gz (12.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

namespaced_enums-0.1.0-py3-none-any.whl (9.3 kB view details)

Uploaded Python 3

File details

Details for the file namespaced_enums-0.1.0.tar.gz.

File metadata

  • Download URL: namespaced_enums-0.1.0.tar.gz
  • Upload date:
  • Size: 12.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for namespaced_enums-0.1.0.tar.gz
Algorithm Hash digest
SHA256 9a7dde383662d5f50ddbabff50b0e96a002b6a4fe9a13390cebbc310006fa583
MD5 3bbafd6512708f86c6c03fade3c02817
BLAKE2b-256 b61549f40c784c8d39b5122ce5a82fa901c2d5e3feb1d3e3a319c3ea7d22a523

See more details on using hashes here.

Provenance

The following attestation bundles were made for namespaced_enums-0.1.0.tar.gz:

Publisher: publish.yml on Waszker/namespaced_enums

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file namespaced_enums-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for namespaced_enums-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2c3ab6ce924f83dfc12f1564dd4f59f065dabe04fad43268914854afef7dad86
MD5 3f1271a226469a0ad4eea37c2cf9b163
BLAKE2b-256 cd47e6f2b7b9db37e14fc758e9f47e6199b46771cc54fd98496b08c32620d55b

See more details on using hashes here.

Provenance

The following attestation bundles were made for namespaced_enums-0.1.0-py3-none-any.whl:

Publisher: publish.yml on Waszker/namespaced_enums

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page