enumerific 0.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright © 2024–2025 Daniel Sissman.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ Metadata-Version: 2.2
2
+ Name: enumerific
3
+ Version: 0.0.0
4
+ License-File: LICENSE.md
@@ -0,0 +1,126 @@
1
+ # Enumerific Enums
2
+
3
+ The `enumerific` library provides several useful extensions to the Python built-in `enums` library.
4
+
5
+ ### Requirements
6
+
7
+ The Enumerific library has been tested with Python 3.9, 3.10, 3.11, 3.12 and 3.13 but may work with some earlier versions such as 3.8, but has not been tested against this version or any earlier. The library is not compatible with Python 2.* or earlier.
8
+
9
+ ### Installation
10
+
11
+ The Enumerific library is available from PyPi, so may be added to a project's dependencies via its `requirements.txt` file or similar by referencing the Enumerific library's name, `enumerific`, or the library may be installed directly into your local runtime environment using `pip install` by entering the following command, and following any prompts:
12
+
13
+ $ pip install enumerific
14
+
15
+ ### Usage
16
+
17
+ To use the Enumerific library, simply import the library and use it like you would the built-in `enum` library as a drop-in replacement:
18
+
19
+ ```
20
+ import enumerific
21
+
22
+ class MyEnum(enumerific.Enum):
23
+ Option1 = "ABC"
24
+ Option2 = "DEF"
25
+
26
+ val = MyEnum.Option1
27
+ ```
28
+
29
+ You can also import the `Enum` class directly from the `enumerific` library and use it directly:
30
+
31
+ ```
32
+ from enumerific import Enum
33
+
34
+ class MyEnum(Enum):
35
+ Option1 = "ABC"
36
+ ...
37
+ ```
38
+
39
+ The Enumerific library's own `Enum` class is a subclass of the built-in `enum.Enum` class, so all of the built-in functionality of `enum.Enum` is available, as well as several additional class methods:
40
+
41
+ * `reconcile(value: object, default: Enum = None, raises: bool = False) -> Enum` – The `reconcile` method allows for an enumeration's value or an enumeration option's name to be reconciled against a matching enumeration option. If the provided value can be matched against one of the enumeration's available options, that option will be returned, otherwise there are two possible behaviours: if the `raises` keyword argument has been set to or left as `False` (its default), the value assigned to the `default` keyword argument will be returned, which may be `None` if no default value has been specified; if the `raises` argument has been set to `True` an `EnumValueError` exception will be raised as an alert that the provided value could not be matched. One can also provide an enumeration option as the input value to the `reconcile` method, and these will be validated and returned as-is.
42
+ * `validate(value: object) -> bool` – The `validate` method takes the same range of input values as the `reconcile` method, and returns `True` when the provided value can be reconciled against an enumeration option, or `False` otherwise.
43
+ * `options() -> list[Enum]` – The `options` method provides easy access to the list of the enumeration's available options.
44
+
45
+ The benefits of being able to validate and reconcile various input values against an enumeration, include allowing for a controlled vocabulary of options to be checked against, and the ability to convert enumeration values into their corresponding enumeration option. This can be especially useful when working with input data where you need to convert those values to their corresponding enumeration options, and to be able to do so without maintaining boilerplate code to perform the matching and assignment.
46
+
47
+ Some examples of use include the following code samples, where each make use of the example `MyEnum` class, defined as follows:
48
+
49
+ ```
50
+ from enumerific import Enum
51
+
52
+ class MyEnum(Enum):
53
+ Option1 = "ABC"
54
+ Option2 = "DEF"
55
+ ```
56
+
57
+ #### Example 1: Reconciling a Value
58
+
59
+ ```
60
+ # Given a string value in this case
61
+ value = "ABC"
62
+
63
+ # Reconcile it to the associated enumeration option
64
+ value = MyEnum.reconcile(value)
65
+
66
+ assert value == MyEnum.Option1 # asserts successfully
67
+ assert value is MyEnum.Option1 # asserts successfully as enums are singletons
68
+ ```
69
+
70
+ #### Example 2: Reconciling an Enumeration Option Name
71
+
72
+ ```
73
+ # Given a string value in this case
74
+ value = "Option1"
75
+
76
+ # Reconcile it to the associated enumeration option
77
+ value = MyEnum.reconcile(value)
78
+
79
+ assert value == MyEnum.Option1 # asserts successfully
80
+ assert value is MyEnum.Option1 # asserts successfully as enums are singletons
81
+ ```
82
+
83
+ #### Example 3: Validating a Value
84
+
85
+ ```
86
+ # The value can be an enumeration option's name, its value, or the enumeration option
87
+ value = "Option1"
88
+ value = "ABC"
89
+ value = MyEnum.Option1
90
+
91
+ if MyEnum.validate(value) is True:
92
+ # do something if the value could be validated
93
+ else:
94
+ # do something else if the value could not be validated
95
+ ```
96
+
97
+ #### Example 4: Iterating Over Enumeration Options
98
+
99
+ ```
100
+ for option in MyEnum.options():
101
+ # do something with each option
102
+ print(option.name, option.value)
103
+ ```
104
+
105
+ ### Unit Tests
106
+
107
+ The Enumerific library includes a suite of comprehensive unit tests which ensure that the library functionality operates as expected. The unit tests were developed with and are run via `pytest`.
108
+
109
+ To ensure that the unit tests are run within a predictable runtime environment where all of the necessary dependencies are available, a [Docker](https://www.docker.com) image is created within which the tests are run. To run the unit tests, ensure Docker and Docker Compose is [installed](https://docs.docker.com/engine/install/), and perform the following commands, which will build the Docker image via `docker compose build` and then run the tests via `docker compose run` – the output of running the tests will be displayed:
110
+
111
+ ```
112
+ $ docker compose build
113
+ $ docker compose run tests
114
+ ```
115
+
116
+ To run the unit tests with optional command line arguments being passed to `pytest`, append the relevant arguments to the `docker compose run tests` command, as follows, for example passing `-vv` to enable verbose output:
117
+
118
+ ```
119
+ $ docker compose run tests -vv
120
+ ```
121
+
122
+ See the documentation for [PyTest](https://docs.pytest.org/en/latest/) regarding available optional command line arguments.
123
+
124
+ ### Copyright & License Information
125
+
126
+ Copyright © 2024–2025 Daniel Sissman; Licensed under the MIT License.
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["setuptools", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [tool.pytest.ini_options]
6
+ minversion = "6.0"
7
+ addopts = "-ra -q"
8
+ testpaths = [
9
+ "tests"
10
+ ]
11
+
12
+ [tool.black]
13
+ line-length = 88
14
+ target-version = ['py310']
15
+ include = '\.pyi?$'
16
+ extend-exclude = '''
17
+ /(
18
+ # The following are specific to Black, you probably don't want those.
19
+ | blib2to3
20
+ | tests/data
21
+ | profiling
22
+ )/
23
+ '''
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = enumerific
3
+ version = file: source/enumerific/version.txt
4
+ description = Simplifies working with Python enums.
5
+ long_description = file: README.md, CHANGELOG.md
6
+ long_description_content_type = text/markdown
7
+ keywords = enum, enumeration, enumerations
8
+ author = Daniel Sissman
9
+ license = MIT
10
+ classifiers =
11
+ License :: OSI Approved :: MIT License
12
+ Programming Language :: Python :: 3
13
+ Programming Language :: Python :: 3.9
14
+ Programming Language :: Python :: 3.10
15
+ Programming Language :: Python :: 3.11
16
+ Programming Language :: Python :: 3.12
17
+ requires-python = ">=3.9"
18
+
19
+ [project.urls]
20
+ Documentation = "https://github.com/bluebinary/enumerific/blob/main/README.md"
21
+ Changelog = "https://github.com/bluebinary/enumerific/blob/main/CHANGELOG.md"
22
+ Repository = "https://github.com/bluebinary/enumerific"
23
+ Issues = "https://github.com/bluebinary/enumerific/issues"
24
+
25
+ [options]
26
+ zip_safe = True
27
+ include_package_data = True
28
+ package_dir =
29
+ =source
30
+ packages = find:
31
+ install_requires =
32
+
33
+ [options.packages.find]
34
+ where = source
35
+
36
+ [egg_info]
37
+ tag_build =
38
+ tag_date = 0
39
+
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env python
2
+
3
+ # Used to create editable installs
4
+ import setuptools
5
+
6
+ setuptools.setup()
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import *
4
+
5
+ import logging
6
+
7
+
8
+ class EnumValueError(ValueError):
9
+ pass
10
+
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class Enum(Enum):
16
+ """An extended Enum class that provides support for validating an Enum value and
17
+ accepting either enumeration class properties as enumeration values or their string
18
+ names or values, and providing straightforward access to the enumeration values an
19
+ Enum class holds."""
20
+
21
+ @classmethod
22
+ def validate(cls, value: Enum | str | int | object) -> bool:
23
+ """Determine if an enum value name or enum value is valid or not"""
24
+
25
+ try:
26
+ return cls.reconcile(value=value, default=None) is not None
27
+ except EnumValueError as exception:
28
+ return False
29
+
30
+ @classmethod
31
+ def reconcile(
32
+ cls,
33
+ value: Enum | str | int | object,
34
+ default: Enum = None,
35
+ raises: bool = False,
36
+ ) -> Enum | None:
37
+ """Reconcile enum values and enum names to their corresponding enum option, as
38
+ well as allowing valid enum options to be returned unmodified; if the provided
39
+ enum option, enum value or enum name cannot be reconciled and if a default value
40
+ has been provided, the default value will be returned instead and a warning
41
+ message will be logged, otherwise an EnumValueError exception will be raised."""
42
+
43
+ if isinstance(value, str):
44
+ for prop, enumeration in cls.__members__.items():
45
+ if enumeration.name.casefold() == value.casefold():
46
+ return enumeration
47
+ elif (
48
+ isinstance(enumeration.value, str)
49
+ and enumeration.value.casefold() == value.casefold()
50
+ ):
51
+ return enumeration
52
+ elif isinstance(value, int) and not isinstance(value, bool):
53
+ for prop, enumeration in cls.__members__.items():
54
+ if isinstance(enumeration.value, int) and enumeration.value == value:
55
+ return enumeration
56
+ elif isinstance(value, bool):
57
+ for prop, enumeration in cls.__members__.items():
58
+ if enumeration.value is value:
59
+ return enumeration
60
+ elif isinstance(value, cls):
61
+ if value in cls:
62
+ return value
63
+ elif not value is None:
64
+ for prop, enumeration in cls.__members__.items():
65
+ if enumeration.value == value:
66
+ return enumeration
67
+
68
+ if value is not None:
69
+ if raises is True:
70
+ raise EnumValueError(
71
+ "The provided value, %r, is invalid and does not correspond with this enumeration's options!"
72
+ % (value)
73
+ )
74
+ else:
75
+ logger.warning(
76
+ "The provided value, %r, is invalid, but a default, %r, has been provided, and will be returned instead!",
77
+ value,
78
+ default,
79
+ )
80
+
81
+ return default
82
+
83
+ @classmethod
84
+ def options(cls) -> list[Enum]:
85
+ """Provide straightforward access to the list of enumeration options"""
86
+
87
+ return cls.__members__.values()
@@ -0,0 +1,4 @@
1
+ Metadata-Version: 2.2
2
+ Name: enumerific
3
+ Version: 0.0.0
4
+ License-File: LICENSE.md
@@ -0,0 +1,12 @@
1
+ LICENSE.md
2
+ README.md
3
+ pyproject.toml
4
+ setup.cfg
5
+ setup.py
6
+ source/enumerific/__init__.py
7
+ source/enumerific.egg-info/PKG-INFO
8
+ source/enumerific.egg-info/SOURCES.txt
9
+ source/enumerific.egg-info/dependency_links.txt
10
+ source/enumerific.egg-info/top_level.txt
11
+ source/enumerific.egg-info/zip-safe
12
+ tests/test_enumerific_library.py
@@ -0,0 +1 @@
1
+ enumerific
@@ -0,0 +1,135 @@
1
+ import pytest
2
+ import enum
3
+ import enumerific
4
+
5
+
6
+ def test_type(EnumSampleA, EnumSampleB):
7
+ """Ensure that Enumerific `Enum` classes are valid `enum.Enum` and `enumerific.Enum`
8
+ classes by checking their inheritance hierarchy conforms to that which is expected
9
+ """
10
+
11
+ assert issubclass(EnumSampleA, enum.Enum)
12
+ assert issubclass(EnumSampleA, enumerific.Enum)
13
+ assert issubclass(EnumSampleB, enum.Enum)
14
+ assert issubclass(EnumSampleB, enumerific.Enum)
15
+
16
+
17
+ def test_type_equivalence(EnumSampleA, EnumSampleB):
18
+ """Ensure that the sample Enumerific `Enum` subclasses conform to their expected equivalence"""
19
+
20
+ assert EnumSampleA is EnumSampleA
21
+ assert EnumSampleA is not EnumSampleB
22
+ assert EnumSampleB is EnumSampleB
23
+ assert EnumSampleB is not EnumSampleA
24
+
25
+
26
+ def test_reconciliation_success(EnumSampleA):
27
+ """Ensure that the enumeration values reconcile to their expected enumeration options"""
28
+
29
+ assert EnumSampleA.reconcile("Value1") is EnumSampleA.Value1
30
+ assert EnumSampleA.reconcile("Value2") is EnumSampleA.Value2
31
+ assert EnumSampleA.reconcile(3) is EnumSampleA.Value3
32
+ assert EnumSampleA.reconcile(EnumSampleA.Value2) is EnumSampleA.Value2
33
+
34
+
35
+ def test_reconciliation_failure(EnumSampleA):
36
+ """Ensure that the enumeration values reconcile to their expected enumeration options"""
37
+
38
+ assert EnumSampleA.reconcile("Value3", default=None) is not EnumSampleA.Value1
39
+ assert EnumSampleA.reconcile("123", default=None) is not EnumSampleA.Value1
40
+ assert EnumSampleA.reconcile(123, default=None) is not EnumSampleA.Value1
41
+ assert EnumSampleA.reconcile(True, default=None) is not EnumSampleA.Value1
42
+
43
+
44
+ def test_reconciliation_without_default_fallback_success(EnumSampleA):
45
+ """Ensure that attempting to reconcile an invalid enumeration value, while providing
46
+ no default fallback value results in an EnumValueError exception being raised"""
47
+
48
+ assert EnumSampleA.reconcile(4, default=EnumSampleA.Value1) is EnumSampleA.Value1
49
+ assert EnumSampleA.reconcile(4, default=EnumSampleA.Value2) is EnumSampleA.Value2
50
+ assert EnumSampleA.reconcile(4, default=EnumSampleA.Value3) is EnumSampleA.Value3
51
+
52
+
53
+ def test_reconciliation_failure_with_raises_exception_disabled(EnumSampleA):
54
+ """Ensure that attempting to reconcile an invalid enumeration value, while providing
55
+ a None default fallback value results in a None value being returned"""
56
+
57
+ # Note the default value for the 'raises' keyword argument is False (disabled)
58
+ assert EnumSampleA.reconcile("Value4", default=None) is None
59
+
60
+
61
+ def test_reconciliation_failure_with_raises_exception_enabled(EnumSampleA):
62
+ """Ensure that attempting to reconcile an invalid enumeration value, while calling
63
+ the reconcile method with the raises=True argument raises the expected exception"""
64
+
65
+ # Set the 'raises' keyword argument to False to prevent raising an exception; this
66
+ # is the default behaviour for the reconcile function, but is used below for clarity
67
+ assert EnumSampleA.reconcile("Value4", raises=False) is None
68
+
69
+ with pytest.raises(enumerific.EnumValueError) as exception:
70
+ # Set the 'raises' keyword argument to True to enable raising an exception; this
71
+ # overrides the default exception behaviour, and must be specified to override
72
+ assert EnumSampleA.reconcile("Value4", raises=True) is None
73
+
74
+ assert (
75
+ str(exception)
76
+ == "EnumValueError: The provided value, 'Value4', is invalid and does not correspond with this enumeration's options!"
77
+ )
78
+
79
+
80
+ def test_validation_success(EnumSampleA, EnumSampleB):
81
+ """Ensure that values which correspond to enumeration options validate as True"""
82
+
83
+ assert EnumSampleA.validate("Value1") is True
84
+ assert EnumSampleA.validate("Value2") is True
85
+ assert EnumSampleA.validate(3) is True
86
+ assert EnumSampleA.validate(EnumSampleA.Value1) is True
87
+ assert EnumSampleA.validate(EnumSampleA.Value2) is True
88
+ assert EnumSampleA.validate(EnumSampleA.Value3) is True
89
+ assert EnumSampleA.validate("Value4") is False
90
+
91
+ assert EnumSampleB.validate("Value1") is True
92
+ assert EnumSampleB.validate("Value2") is True
93
+ assert EnumSampleB.validate("Value3") is True
94
+ assert EnumSampleB.validate("Value4") is True
95
+
96
+
97
+ def test_validation_failure(EnumSampleA, EnumSampleB):
98
+ """Ensure that values which do not correspond to enumeration options validate as False"""
99
+
100
+ assert EnumSampleA.validate("Value4") is False
101
+ assert EnumSampleA.validate(1234243) is False
102
+ assert EnumSampleA.validate(EnumSampleB.Value1) is False
103
+
104
+
105
+ def test_iteration(EnumSampleA, EnumSampleB):
106
+ """Ensure that iterating over the enumeration options produces the expected result"""
107
+
108
+ options = {}
109
+
110
+ for option in EnumSampleA.options():
111
+ options[option.name] = option.value
112
+
113
+ # EnumSampleA defined in conftest.py has three options
114
+ assert len(options) == 3
115
+
116
+ assert options == {
117
+ "Value1": "value1",
118
+ "Value2": "value2",
119
+ "Value3": 3,
120
+ }
121
+
122
+ options = {}
123
+
124
+ for option in EnumSampleB.options():
125
+ options[option.name] = option.value
126
+
127
+ # EnumSampleB defined in conftest.py has four options
128
+ assert len(options) == 4
129
+
130
+ assert options == {
131
+ "Value1": "value1",
132
+ "Value2": "value2",
133
+ "Value3": 3,
134
+ "Value4": 4.5678,
135
+ }