Skip to content

Commit ddc5b21

Browse files
🎈 Add Docstrings to Anrede Enum Members (#75)
Co-authored-by: Kevin <68426071+hf-krechan@users.noreply.github.com>
1 parent f8c02f5 commit ddc5b21

File tree

3 files changed

+120
-7
lines changed

3 files changed

+120
-7
lines changed

‎src/bo4e/enum/anrede.py‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
# pylint: disable=missing-module-docstring
2-
from bo4e.enum.strenum import StrEnum
2+
from bo4e.enum.strenum import DocumentedStrEnum
33

44

5-
class Anrede(StrEnum):
5+
class Anrede(DocumentedStrEnum):
66
"""
77
Übersicht möglicher Anreden, z.B. eines Geschäftspartners.
88
"""
99

10-
HERR = "HERR"
11-
FRAU = "FRAU"
12-
EHELEUTE = "EHELEUTE"
13-
FIRMA = "FIRMA"
14-
INDIVIDUELL = "INDIVIDUELL"
10+
HERR = "HERR", "Herr"
11+
FRAU = "FRAU", "Frau"
12+
EHELEUTE = "EHELEUTE", "Eheleute"
13+
FIRMA = "FIRMA", "Firma"
14+
INDIVIDUELL = "INDIVIDUELL", 'Individuell (z.B. "Profx")'

‎src/bo4e/enum/strenum.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,25 @@ class StrEnum(str, Enum):
1212
"""
1313

1414
# see https://docs.python.org/3/library/enum.html?highlight=strenum#others
15+
16+
17+
# pylint: disable=too-few-public-methods
18+
class DocumentedStrEnum(StrEnum):
19+
"""
20+
A StrEnum that has docstrings attached to its members.
21+
Use it like this:
22+
class MyDocumentedEnum(DocumentedStrEnum):
23+
Foo = "Serialized Foo", "my good docstring of Foo"
24+
Bar = "Serialized Bar", "bar is not foo (this is a docstring)"
25+
Then wherever MyDocumentedEnum is used in BOs or COMs, the attribute will serialize as either "Serialized Foo"
26+
or "Serialized Bar". And inspect.getdocs(MyDocumentedEnum.Foo) will return the respective docstring.
27+
This is unittested.
28+
"""
29+
30+
# see https://stackoverflow.com/a/50473952/10009545
31+
def __new__(cls, value, doc=None):
32+
self = str.__new__(cls, value) # calling super().__new__(value) here would fail
33+
self._value_ = value
34+
if doc is not None:
35+
self.__doc__ = doc
36+
return self

‎tests/test_enums.py‎

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import inspect
2+
import re
3+
from pathlib import Path
4+
from typing import List, Optional, TypeVar
5+
6+
import pytest # type:ignore[import]
7+
8+
from bo4e.enum import anrede
9+
from bo4e.enum.anrede import Anrede
10+
from bo4e.enum.strenum import StrEnum
11+
12+
13+
class TestEnums:
14+
"""
15+
A test class that checks the enum construction.
16+
"""
17+
18+
starts_with_whitespace_pattern = re.compile(r"^[\s\n]+")
19+
ends_with_whitespace_pattern = re.compile(r"[\s\n]+$")
20+
21+
TEnum = TypeVar("TEnum", bound=StrEnum)
22+
23+
@staticmethod
24+
def _get_all_enum_classes() -> List[TEnum]:
25+
"""
26+
returns a list of all bo4e.enum classes
27+
"""
28+
arbitrary_enum_module_path = Path(anrede.__file__)
29+
result = []
30+
for python_file in arbitrary_enum_module_path.parent.glob("*.py"):
31+
# don't ask me why. but it works.
32+
enum_module = __import__("bo4e.enum." + python_file.name.split(".")[0])
33+
for _, member in inspect.getmembers(enum_module.enum):
34+
if inspect.ismodule(member):
35+
candidate = inspect.getmembers(member)[0][1]
36+
if inspect.isclass(candidate):
37+
result.append(candidate)
38+
return result
39+
40+
@staticmethod
41+
def _get_class_doc(enum_class: TEnum) -> Optional[str]:
42+
"""
43+
asserts that enum class is a class and returns the class' docstring
44+
"""
45+
assert inspect.isclass(enum_class)
46+
return inspect.getdoc(enum_class)
47+
48+
def test_enum_classes_docstrings(self):
49+
"""
50+
Tests that the docstrings of the enum classes do not start with whitespace or blank lines.
51+
"""
52+
all_enums = TestEnums._get_all_enum_classes()
53+
assert len(all_enums) > 100 # just to be sure we're not using the wrong directory or path
54+
for enum_class in all_enums:
55+
docstring = TestEnums._get_class_doc(enum_class)
56+
assert docstring is not None
57+
assert not TestEnums.starts_with_whitespace_pattern.match(docstring)
58+
assert not TestEnums.ends_with_whitespace_pattern.match(docstring)
59+
60+
@pytest.mark.parametrize(
61+
"enum_member, expected_docstring",
62+
[
63+
pytest.param(Anrede.HERR, "Herr"),
64+
pytest.param(Anrede.INDIVIDUELL, 'Individuell (z.B. "Profx")'),
65+
],
66+
)
67+
def test_enum_member_docstrings_explicitly(self, enum_member: TEnum, expected_docstring: Optional[str]):
68+
"""
69+
Test the docstrings of the enum members explicitly.
70+
if the general approach (using DocumentedStrEnum) works for single members, it will also work for all enums,
71+
which are constructed similarly.
72+
"""
73+
assert inspect.getdoc(enum_member) == expected_docstring
74+
75+
def test_enum_members_are_all_documented(self):
76+
"""
77+
The class docstrings are enforced using pylint but the docstrings of enum members are not covered by pylint.
78+
"""
79+
all_enums = self._get_all_enum_classes()
80+
for enum_class in all_enums:
81+
class_docstring = TestEnums._get_class_doc(enum_class)
82+
for enum_member in enum_class:
83+
member_docstring = inspect.getdoc(enum_member)
84+
assert (
85+
member_docstring != class_docstring
86+
), f"Docstring of Enum member {enum_member} must not be the same as the class docstring"
87+
assert member_docstring is not None
88+
assert member_docstring != ""
89+
assert not TestEnums.starts_with_whitespace_pattern.match(member_docstring)
90+
assert not TestEnums.ends_with_whitespace_pattern.match(member_docstring)
91+
break # proof this works just for the wip anrede

0 commit comments

Comments
 (0)