Repository: XKNX/xknx
Pull request #918 (2022-04-16 04:54:57)

Include base class in `DPTBase.parse_transcoder()` lookup
<!--
  You are awesome! Thanks for contributing to our project!
  Please, DO NOT DELETE ANY TEXT from this template!.
-->
## Description
<!--
Please include a summary of the change and which issue is fixed.
Please also include relevant motivation and context.
List any dependencies that are required for this change.
-->
Include base class in `DPTBase.parse_transcoder()` lookup.

```py
>>> for dpt in DPTString.dpt_class_tree():
...     print(dpt)
...
<class 'xknx.dpt.dpt_string.DPTString'>  # <- this would have not been included
<class 'xknx.dpt.dpt_string.DPTLatin1'>
```
so `DPTString.parse_transcoder("string")` yields `DPTString`. Same for `DPT4ByteFloat` etc. where the implementation is done in a non-abstract DPTBase subclass.

## Type of change
<!--
Please tick the applicable options.
NOTE: Ticking multiple options most likely indicates
that your change is to big and it is suggested to split it into several smaller PRs.
-->

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update

## Checklist:

- [x] I have performed a self-review of my own code
- [ ] The documentation has been adjusted accordingly
- [x] The changes generate no new warnings
- [x] Tests have been added that prove the fix is effective or that the feature works
- [x] The changes are documented in the changelog
- [ ] The Homeassistant plugin has been adjusted in case of new config options


codecov[bot]: # [Codecov](https://codecov.io/gh/XKNX/xknx/pull/918?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) Report
> Merging [#918](https://codecov.io/gh/XKNX/xknx/pull/918?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (075ad02) into [main](https://codecov.io/gh/XKNX/xknx/commit/f58a62019964d6a769ffb053d5329bebdfc77c47?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (f58a620) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/XKNX/xknx/pull/918/graphs/tree.svg?width=650&height=150&src=pr&token=irWbIygS84&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)](https://codecov.io/gh/XKNX/xknx/pull/918?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)

```diff
@@           Coverage Diff           @@
##             main     #918   +/-   ##
=======================================
  Coverage   95.62%   95.62%           
=======================================
  Files         130      130           
  Lines        7810     7815    +5     
=======================================
+ Hits         7468     7473    +5     
  Misses        342      342           
```


| [Impacted Files](https://codecov.io/gh/XKNX/xknx/pull/918?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) | Coverage Δ | |
|---|---|---|
| [xknx/dpt/dpt.py](https://codecov.io/gh/XKNX/xknx/pull/918/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvZHB0LnB5) | `100.00% <100.00%> (ø)` | |


Patch:
diff --git a/changelog.md b/changelog.md
index 443f190b..b80c9db3 100644
--- a/changelog.md
+++ b/changelog.md
@@ -4,6 +4,7 @@
 
 ### Internal
 
+- Include base class in `DPTBase.parse_transcoder()` lookup
 - Move `levels` instance attribute form `GroupAddress` to `address_format` class variable
 - Remove xknx form every class in the knxip package: CEMIFrame, KNXIPFrame and KNXIPBody (and subclasses)
 
diff --git a/xknx/dpt/dpt.py b/xknx/dpt/dpt.py
index fbe69124..3e211dbb 100644
--- a/xknx/dpt/dpt.py
+++ b/xknx/dpt/dpt.py
@@ -86,6 +86,13 @@ class DPTBase(ABC):
             if not isabstract(subclass):
                 yield subclass
 
+    @classmethod
+    def dpt_class_tree(cls: T) -> Iterator[T]:
+        """Yield class, all subclasses and their subclasses that are not abstract."""
+        if not isabstract(cls):
+            yield cls
+        yield from cls.__recursive_subclasses__()
+
     @classmethod
     def has_distinct_dpt_numbers(cls) -> bool:
         """Return True if dpt numbers are defined (not inherited)."""
@@ -101,7 +108,7 @@ class DPTBase(ABC):
         cls: T, dpt_main: int, dpt_sub: int | None = None
     ) -> T | None:
         """Return Class reference of DPTBase subclass with matching DPT number."""
-        for dpt in cls.__recursive_subclasses__():
+        for dpt in cls.dpt_class_tree():
             if dpt.has_distinct_dpt_numbers():
                 if dpt_main == dpt.dpt_main_number and dpt_sub == dpt.dpt_sub_number:
                     return dpt
@@ -110,7 +117,7 @@ class DPTBase(ABC):
     @classmethod
     def transcoder_by_value_type(cls: T, value_type: str) -> T | None:
         """Return Class reference of DPTBase subclass with matching value_type."""
-        for dpt in cls.__recursive_subclasses__():
+        for dpt in cls.dpt_class_tree():
             if dpt.has_distinct_value_type():
                 if value_type == dpt.value_type:
                     return dpt


Test patch:
diff --git a/test/dpt_tests/dpt_test.py b/test/dpt_tests/dpt_test.py
index bd4c868f..dc200277 100644
--- a/test/dpt_tests/dpt_test.py
+++ b/test/dpt_tests/dpt_test.py
@@ -89,13 +89,18 @@ class TestDPTBase:
     """Test class for transcoder base object."""
 
     def test_dpt_abstract_subclasses_ignored(self):
-        """Test if abstract base classes are ignored by __recursive_subclasses__."""
-        for dpt in DPTBase.__recursive_subclasses__():
-            assert dpt is not DPTNumeric
+        """Test if abstract base classes are ignored by dpt_class_tree and __recursive_subclasses__."""
+        for dpt in DPTBase.dpt_class_tree():
+            assert dpt not in (DPTBase, DPTNumeric)
+
+    @pytest.mark.parametrize("dpt_class", [DPTString, DPT2ByteFloat])
+    def test_dpt_non_abstract_baseclass_included(self, dpt_class):
+        """Test if non-abstract base classes is included by dpt_class_tree."""
+        assert dpt_class in dpt_class.dpt_class_tree()
 
     def test_dpt_subclasses_definition_types(self):
         """Test value_type and dpt_*_number values for correct type in subclasses of DPTBase."""
-        for dpt in DPTBase.__recursive_subclasses__():
+        for dpt in DPTBase.dpt_class_tree():
             if dpt.value_type is not None:
                 assert isinstance(
                     dpt.value_type, str
@@ -112,7 +117,7 @@ class TestDPTBase:
     def test_dpt_subclasses_no_duplicate_value_types(self):
         """Test for duplicate value_type values in subclasses of DPTBase."""
         value_types = []
-        for dpt in DPTBase.__recursive_subclasses__():
+        for dpt in DPTBase.dpt_class_tree():
             if dpt.value_type is not None:
                 value_types.append(dpt.value_type)
         assert len(value_types) == len(set(value_types))
@@ -120,7 +125,7 @@ class TestDPTBase:
     def test_dpt_subclasses_no_duplicate_dpt_number(self):
         """Test for duplicate value_type values in subclasses of DPTBase."""
         dpt_tuples = []
-        for dpt in DPTBase.__recursive_subclasses__():
+        for dpt in DPTBase.dpt_class_tree():
             if dpt.dpt_main_number is not None and dpt.dpt_sub_number is not None:
                 dpt_tuples.append((dpt.dpt_main_number, dpt.dpt_sub_number))
         assert len(dpt_tuples) == len(set(dpt_tuples))
@@ -157,7 +162,7 @@ class TestDPTBase:
 class TestDPTNumeric:
     """Test class for numeric transcoder base object."""
 
-    @pytest.mark.parametrize("dpt_class", DPTNumeric.__recursive_subclasses__())
+    @pytest.mark.parametrize("dpt_class", DPTNumeric.dpt_class_tree())
     def test_values(self, dpt_class):
         """Test boundary values are set for numeric definitions (because mypy doesn't)."""
 
diff --git a/test/remote_value_tests/remote_value_string_test.py b/test/remote_value_tests/remote_value_string_test.py
index 29194e4a..64154953 100644
--- a/test/remote_value_tests/remote_value_string_test.py
+++ b/test/remote_value_tests/remote_value_string_test.py
@@ -33,7 +33,11 @@ class TestRemoteValueString:
                 0x00,
             )
         )
-        remote_value_ascii = RemoteValueString(xknx)
+        remote_value_default = RemoteValueString(xknx)
+        assert remote_value_default.dpt_class == DPTString
+        assert remote_value_default.to_knx("KNX is OK") == dpt_array_string
+
+        remote_value_ascii = RemoteValueString(xknx, value_type="string")
         assert remote_value_ascii.dpt_class == DPTString
         assert remote_value_ascii.to_knx("KNX is OK") == dpt_array_string