From 99dc6c3b0f3c5e5bd56a80cb95db3565015befa3 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Wed, 31 Jul 2024 08:11:16 +0200 Subject: [PATCH 1/3] - updated dependencies and reformatted some files --- .pre-commit-config.yaml | 11 ++++++-- .ruff.toml | 8 +++--- requirements.txt | 8 +++--- requirements_setup.txt | 6 ++--- setup.py | 34 ++++++++++++------------- src/sml2mqtt/__log__.py | 2 +- src/sml2mqtt/__main__.py | 4 +-- src/sml2mqtt/config/inputs.py | 2 +- src/sml2mqtt/config/operations.py | 4 +-- src/sml2mqtt/const/sml_helpers.py | 2 +- src/sml2mqtt/const/task.py | 2 +- src/sml2mqtt/sml_value/sml_value.py | 2 +- tests/sml_data.py | 4 +-- tests/sml_device/frames/test_frame_1.py | 6 ++--- tests/sml_device/frames/test_frame_2.py | 2 +- tests/sml_device/test_device.py | 8 +++--- tests/sml_device/test_setup_device.py | 6 ++--- tests/test_mqtt_obj.py | 2 +- tests/test_source/test_http.py | 8 +++--- 19 files changed, 65 insertions(+), 56 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d069fb3..3549780 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.6.0 hooks: - id: check-ast - id: check-builtin-literals @@ -14,9 +14,16 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.4 + rev: v0.5.5 hooks: - id: ruff + # I001 [*] Import block is un-sorted or un-formatted + # UP035 [*] Import from {target} instead: {names} + # Q000 [*] Double quote found but single quotes preferred + # Q001 [*] Double quote multiline found but single quotes preferred + args: [ "--select", "I001,UP035,Q000,Q001", "--fix"] + + - id: ruff - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 diff --git a/.ruff.toml b/.ruff.toml index f60ff22..02e66b1 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -7,9 +7,6 @@ src = ["src", "test"] [lint] -# https://docs.astral.sh/ruff/settings/#ignore-init-module-imports -ignore-init-module-imports = true - select = [ "E", "W", # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w "I", # https://docs.astral.sh/ruff/rules/#isort-i @@ -56,6 +53,11 @@ ignore = [ quote-style = "single" +# https://docs.astral.sh/ruff/settings/#lintflake8-quotes +[lint.flake8-quotes] +inline-quotes = "single" +multiline-quotes = "single" + [lint.flake8-builtins] builtins-ignorelist = ["id", "input"] diff --git a/requirements.txt b/requirements.txt index 1334503..5f7eb87 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ -r requirements_setup.txt # Testing -pytest == 8.1.1 -pre-commit == 3.7.0 -pytest-asyncio == 0.23.6 +pytest == 8.3.2 +pre-commit == 3.8.0 +pytest-asyncio == 0.23.8 aioresponses == 0.7.6 # Linter -ruff == 0.4.2 +ruff == 0.5.5 diff --git a/requirements_setup.txt b/requirements_setup.txt index 62b5477..b7ee725 100644 --- a/requirements_setup.txt +++ b/requirements_setup.txt @@ -1,6 +1,6 @@ -aiomqtt == 2.0.1 +aiomqtt == 2.2.0 pyserial-asyncio == 0.6 easyconfig == 0.3.2 -pydantic == 2.7.0 +pydantic == 2.8.2 smllib == 1.4 -aiohttp == 3.9.5 +aiohttp == 3.10.0 diff --git a/setup.py b/setup.py index efc55e3..2a11442 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ # Load version number without importing HABApp def load_version() -> str: version: dict[str, str] = {} - with open("src/sml2mqtt/__version__.py") as fp: + with open('src/sml2mqtt/__version__.py') as fp: exec(fp.read(), version) assert version['__version__'], version return version['__version__'] @@ -29,16 +29,16 @@ def load_req() -> list[str]: readme = Path(__file__).with_name('readme.md') long_description = '' if readme.is_file(): - with readme.open("r", encoding='utf-8') as fh: + with readme.open('r', encoding='utf-8') as fh: long_description = fh.read() setuptools.setup( - name="sml2mqtt", + name='sml2mqtt', version=__version__, - author="spaceman_spiff", + author='spaceman_spiff', # author_email="", - description="A sml (Smart Message Language) energy meter to MQTT bridge. " - "Can read from serial ports or http (e.g. Tibber Pulse).", + description='A sml (Smart Message Language) energy meter to MQTT bridge. ' + 'Can read from serial ports or http (e.g. Tibber Pulse).', keywords=[ 'mqtt', 'sml', @@ -47,8 +47,8 @@ def load_req() -> list[str]: 'tibber' ], long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/spacemanspiff2007/sml2mqtt", + long_description_content_type='text/markdown', + url='https://github.com/spacemanspiff2007/sml2mqtt', project_urls={ 'GitHub': 'https://github.com/spacemanspiff2007/sml2mqtt', }, @@ -57,15 +57,15 @@ def load_req() -> list[str]: python_requires='>=3.10', install_requires=load_req(), classifiers=[ - "Development Status :: 4 - Beta", - "Framework :: AsyncIO", - "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3 :: Only", - "Topic :: Home Automation" + 'Development Status :: 4 - Beta', + 'Framework :: AsyncIO', + 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3 :: Only', + 'Topic :: Home Automation' ], entry_points={ 'console_scripts': [ diff --git a/src/sml2mqtt/__log__.py b/src/sml2mqtt/__log__.py index 6c11b47..92b2fb5 100644 --- a/src/sml2mqtt/__log__.py +++ b/src/sml2mqtt/__log__.py @@ -37,7 +37,7 @@ def setup_log(): for device in sml2mqtt.CONFIG.inputs: # Name of the longest logger, should be the device status chars = max(len(get_logger(device.get_device_name()).getChild('status').name), chars) - log_format = logging.Formatter("[{asctime:s}] [{name:" + str(chars) + "s}] {levelname:8s} | {message:s}", style='{') + log_format = logging.Formatter('[{asctime:s}] [{name:' + str(chars) + 's}] {levelname:8s} | {message:s}', style='{') # File Handler file_path = sml2mqtt.CONFIG.logging.file diff --git a/src/sml2mqtt/__main__.py b/src/sml2mqtt/__main__.py index d78ec04..47e1f64 100644 --- a/src/sml2mqtt/__main__.py +++ b/src/sml2mqtt/__main__.py @@ -48,7 +48,7 @@ async def a_main(): def main() -> int | str: # This is needed to make async-mqtt work # see https://github.com/sbtinstruments/asyncio-mqtt - if sys.platform.lower() == "win32" or os.name.lower() == "nt": + if sys.platform.lower() == 'win32' or os.name.lower() == 'nt': asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) # Load config @@ -74,7 +74,7 @@ def main() -> int | str: return 0 -if __name__ == "__main__": +if __name__ == '__main__': ret = main() log.info(f'Closed with return code {ret}') sys.exit(ret) diff --git a/src/sml2mqtt/config/inputs.py b/src/sml2mqtt/config/inputs.py index 8091fa5..5573bb4 100644 --- a/src/sml2mqtt/config/inputs.py +++ b/src/sml2mqtt/config/inputs.py @@ -70,7 +70,7 @@ def _val_bytesize(cls, v): @override def get_device_name(self) -> str: - return self.url.split("/")[-1] + return self.url.split('/')[-1] class HttpSourceSettings(SmlSourceSettingsBase): diff --git a/src/sml2mqtt/config/operations.py b/src/sml2mqtt/config/operations.py index 44f01ef..6c706bd 100644 --- a/src/sml2mqtt/config/operations.py +++ b/src/sml2mqtt/config/operations.py @@ -148,8 +148,8 @@ def generate_day_names() -> dict[str, int]: day_names.update({date(2001, 1, i).strftime('%A')[:3]: i for i in range(1, 8)}) # abbreviations in German and English - day_names.update({"Mo": 1, "Di": 2, "Mi": 3, "Do": 4, "Fr": 5, "Sa": 6, "So": 7}) - day_names.update({"Mon": 1, "Tue": 2, "Wed": 3, "Thu": 4, "Fri": 5, "Sat": 6, "Sun": 7}) + day_names.update({'Mo': 1, 'Di': 2, 'Mi': 3, 'Do': 4, 'Fr': 5, 'Sa': 6, 'So': 7}) + day_names.update({'Mon': 1, 'Tue': 2, 'Wed': 3, 'Thu': 4, 'Fri': 5, 'Sat': 6, 'Sun': 7}) return {k.lower(): v for k, v in day_names.items()} diff --git a/src/sml2mqtt/const/sml_helpers.py b/src/sml2mqtt/const/sml_helpers.py index 2c37fdc..7c80086 100644 --- a/src/sml2mqtt/const/sml_helpers.py +++ b/src/sml2mqtt/const/sml_helpers.py @@ -59,7 +59,7 @@ def create(cls, timestamp: float, values: Iterable[SmlListEntry]): def __init__(self, timestamp: float): self.timestamp: Final = timestamp - self.values: dict[str, SmlListEntry] = {} + self.values: Final[dict[str, SmlListEntry]] = {} def __getattr__(self, item: str) -> SmlListEntry: return self.values[item] diff --git a/src/sml2mqtt/const/task.py b/src/sml2mqtt/const/task.py index 27ad6e6..f38548f 100644 --- a/src/sml2mqtt/const/task.py +++ b/src/sml2mqtt/const/task.py @@ -57,7 +57,7 @@ def __init__(self, coro: Callable[[], Awaitable], *, name: str): @property def is_running(self) -> bool: - if (task := self._task) is None or task.cancelled(): + if (task := self._task) is None or task.cancelled(): # noqa: SIM103 return False return True diff --git a/src/sml2mqtt/sml_value/sml_value.py b/src/sml2mqtt/sml_value/sml_value.py index 656f1c2..86e7c4d 100644 --- a/src/sml2mqtt/sml_value/sml_value.py +++ b/src/sml2mqtt/sml_value/sml_value.py @@ -4,7 +4,7 @@ from sml2mqtt.const import SmlFrameValues from sml2mqtt.mqtt import MqttObj -from sml2mqtt.sml_value.base import OperationContainerBase, SmlValueInfo, ValueOperationBase +from sml2mqtt.sml_value.base import OperationContainerBase, SmlValueInfo class SmlValue(OperationContainerBase): diff --git a/tests/sml_data.py b/tests/sml_data.py index f2524c0..447767c 100644 --- a/tests/sml_data.py +++ b/tests/sml_data.py @@ -80,7 +80,7 @@ def sml_frame_2_analyze(sml_frame_2): @pytest.fixture() def sml_data_1_analyze(): - return """ + return ''' Received Frame -> b'760501188e6162006200726500000101760101070000000000000b00000000000000000000010163687700760501188e626200620072650000070177010b000000000000000000000172620165002ec3f47a77078181c78203ff010101010445425a0177070100000009ff010101010b000000000000000000000177070100010800ff6401018001621e52fb690000000a7ac1bc170177070100010801ff0101621e52fb690000000a74b1ea770177070100010802ff0101621e52fb6900000000060fd1a00177070100020800ff6401018001621e52fb69000000000d19e1c00177070100100700ff0101621b52fe55000089d90177070100240700ff0101621b52fe55000020220177070100380700ff0101621b52fe5500000a9201770701004c0700ff0101621b52fe5500005f2501010163810200760501188e636200620072650000020171016325fc00' @@ -206,4 +206,4 @@ def sml_data_1_analyze(): message_body global_signature: None crc16 : 9724 -""" +''' diff --git a/tests/sml_device/frames/test_frame_1.py b/tests/sml_device/frames/test_frame_1.py index 4eca213..f7fd185 100644 --- a/tests/sml_device/frames/test_frame_1.py +++ b/tests/sml_device/frames/test_frame_1.py @@ -16,7 +16,7 @@ async def test_frame_no_match_obis_id(no_mqtt, caplog, monkeypatch, sml_frame_1, device.on_source_data(None) - msg = "\n".join(x.msg for x in caplog.records) + msg = '\n'.join(x.msg for x in caplog.records) assert msg.removeprefix(sml_frame_1_analyze) == ''' Found none of the following obis ids in the sml frame: 0100000009ff, 01006001ffff @@ -36,7 +36,7 @@ async def test_frame_no_config(no_mqtt, caplog, monkeypatch, sml_frame_1, arg_an device.on_source_data(None) - msg = "\n".join(x.msg for x in caplog.records) + msg = '\n'.join(x.msg for x in caplog.records) assert msg.removeprefix(sml_frame_1_analyze) == ''' Found obis id 0100600100ff in the sml frame @@ -109,7 +109,7 @@ async def test_frame_with_config(no_mqtt, caplog, monkeypatch, sml_frame_1, arg_ device.on_source_data(None) - msg = "\n".join(x.msg for x in caplog.records) + msg = '\n'.join(x.msg for x in caplog.records) print(msg) diff --git a/tests/sml_device/frames/test_frame_2.py b/tests/sml_device/frames/test_frame_2.py index 734b66c..ad334e2 100644 --- a/tests/sml_device/frames/test_frame_2.py +++ b/tests/sml_device/frames/test_frame_2.py @@ -15,7 +15,7 @@ async def test_frame_no_match_obis_id(no_mqtt, caplog, monkeypatch, sml_frame_2, device.on_source_data(None) - msg = "\n".join(x.msg for x in caplog.records) + msg = '\n'.join(x.msg for x in caplog.records) assert msg.removeprefix(sml_frame_2_analyze) == ''' get_obis failed - try parsing frame diff --git a/tests/sml_device/test_device.py b/tests/sml_device/test_device.py index b52b259..1c56594 100644 --- a/tests/sml_device/test_device.py +++ b/tests/sml_device/test_device.py @@ -16,9 +16,9 @@ async def test_device_analyze(no_mqtt, caplog, sml_data_1, arg_analyze, sml_data device.on_source_data(sml_data_1[i: i + chunk_size]) # This is what will be reported - msg = "\n".join(x.msg for x in filter(lambda x: x.name == 'sml.mqtt.pub', caplog.records)) + msg = '\n'.join(x.msg for x in filter(lambda x: x.name == 'sml.mqtt.pub', caplog.records)) - assert '\n' + msg == """ + assert '\n' + msg == ''' 00000000000000000000/0100000009ff: 00000000000000000000 (QOS: 0, retain: False) 00000000000000000000/0100010800ff: 450.09189911 (QOS: 0, retain: False) 00000000000000000000/0100010801ff: 449.07489911 (QOS: 0, retain: False) @@ -29,9 +29,9 @@ async def test_device_analyze(no_mqtt, caplog, sml_data_1, arg_analyze, sml_data 00000000000000000000/0100380700ff: 27.06 (QOS: 0, retain: False) 00000000000000000000/01004c0700ff: 243.57 (QOS: 0, retain: False) 00000000000000000000/status: OK (QOS: 0, retain: False) -00000000000000000000/status: SHUTDOWN (QOS: 0, retain: False)""" +00000000000000000000/status: SHUTDOWN (QOS: 0, retain: False)''' - msg = "\n".join(x.msg for x in filter(lambda x: x.name == 'sml.device_name', caplog.records)) + msg = '\n'.join(x.msg for x in filter(lambda x: x.name == 'sml.device_name', caplog.records)) assert msg.removeprefix(sml_data_1_analyze) == ''' Found obis id 0100000009ff in the sml frame diff --git a/tests/sml_device/test_setup_device.py b/tests/sml_device/test_setup_device.py index afbeb08..ccc83c0 100644 --- a/tests/sml_device/test_setup_device.py +++ b/tests/sml_device/test_setup_device.py @@ -26,11 +26,11 @@ def test_warnings(no_mqtt, caplog, sml_frame_1_values): setup_device(device, sml_frame_1_values, device_cfg, general_cfg) # This is what will be reported - msg = "\n".join( + msg = '\n'.join( x.msg for x in filter(lambda x: x.name == 'sml.test_device' and x.levelno == logging.WARNING, caplog.records) ) - assert '\n' + msg + '\n' == """ + assert '\n' + msg + '\n' == ''' Config for 0100000009ff found but 0100000009ff is also marked to be skipped Config for 0100000009ff found but 0100000009ff was not reported by the frame -""" +''' diff --git a/tests/test_mqtt_obj.py b/tests/test_mqtt_obj.py index 86adc0f..3ad96e4 100644 --- a/tests/test_mqtt_obj.py +++ b/tests/test_mqtt_obj.py @@ -60,6 +60,6 @@ def test_check_for_duplicate_messages(caplog): check_for_duplicate_topics(parent) - msg = "\n".join(x.msg for x in caplog.records) + msg = '\n'.join(x.msg for x in caplog.records) assert msg == 'Topic "base/child" is already configured!' diff --git a/tests/test_source/test_http.py b/tests/test_source/test_http.py index eb9c338..31d0e29 100644 --- a/tests/test_source/test_http.py +++ b/tests/test_source/test_http.py @@ -15,7 +15,7 @@ def source(device_mock): return HttpSource(device_mock, 'http://localhost:39999', interval=0.020, auth=None, timeout=ClientTimeout(0.5)) -@pytest.mark.skipif(sys.platform.lower() != "win32", reason="It's a mystery why this fails in CI") +@pytest.mark.skipif(sys.platform.lower() != 'win32', reason="It's a mystery why this fails in CI") async def test_200(sml_data_1, device_mock, source): with aioresponses() as m: @@ -34,7 +34,7 @@ async def test_200(sml_data_1, device_mock, source): await close_session() -@pytest.mark.skipif(sys.platform.lower() != "win32", reason="It's a mystery why this fails in CI") +@pytest.mark.skipif(sys.platform.lower() != 'win32', reason="It's a mystery why this fails in CI") async def test_400_then_200(sml_data_1, device_mock, source): with aioresponses() as m: @@ -55,7 +55,7 @@ async def test_400_then_200(sml_data_1, device_mock, source): await close_session() -@pytest.mark.skipif(sys.platform.lower() != "win32", reason="It's a mystery why this fails in CI") +@pytest.mark.skipif(sys.platform.lower() != 'win32', reason="It's a mystery why this fails in CI") async def test_400(device_mock, source): with aioresponses() as m: @@ -79,7 +79,7 @@ def test_error_repr(): assert str(HttpStatusError(404)) == 'HttpStatusError: 404' -@pytest.mark.skipif(sys.platform.lower() != "win32", reason="It's a mystery why this fails in CI") +@pytest.mark.skipif(sys.platform.lower() != 'win32', reason="It's a mystery why this fails in CI") async def test_timeout(device_mock, source): e = TimeoutError() From f2a278ec74190b4c1100b55ac6f710538faf56f6 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Mon, 5 Aug 2024 13:52:05 +0200 Subject: [PATCH 2/3] 3.1 --- docs/requirements.txt | 6 +++--- readme.md | 4 ++++ requirements.txt | 2 +- requirements_setup.txt | 2 +- src/sml2mqtt/__main__.py | 7 ++++++- src/sml2mqtt/__version__.py | 2 +- src/sml2mqtt/sml_device/sml_devices.py | 3 +++ 7 files changed, 19 insertions(+), 7 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 02e630c..680748c 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,6 @@ # Packages required to build the documentation -sphinx == 7.2.6 -sphinx-autodoc-typehints == 2.0.0 +sphinx == 7.4.7 +sphinx-autodoc-typehints == 2.2.3 sphinx_rtd_theme == 2.0.0 sphinx-exec-code == 0.12 -autodoc_pydantic == 2.1.0 +autodoc_pydantic == 2.2.0 diff --git a/readme.md b/readme.md index a778969..856351e 100644 --- a/readme.md +++ b/readme.md @@ -21,6 +21,10 @@ To read from the serial port an IR to USB reader for energy meter is required. # Changelog +#### 3.1 (2024-08-05) +- Updated dependencies +- Added some small log messages + #### 3.0 (2024-04-24) **BREAKING CHANGE** diff --git a/requirements.txt b/requirements.txt index 5f7eb87..245c0e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,4 @@ pytest-asyncio == 0.23.8 aioresponses == 0.7.6 # Linter -ruff == 0.5.5 +ruff == 0.5.6 diff --git a/requirements_setup.txt b/requirements_setup.txt index b7ee725..46b9c82 100644 --- a/requirements_setup.txt +++ b/requirements_setup.txt @@ -3,4 +3,4 @@ pyserial-asyncio == 0.6 easyconfig == 0.3.2 pydantic == 2.8.2 smllib == 1.4 -aiohttp == 3.10.0 +aiohttp == 3.10.1 diff --git a/src/sml2mqtt/__main__.py b/src/sml2mqtt/__main__.py index 47e1f64..a21c0c0 100644 --- a/src/sml2mqtt/__main__.py +++ b/src/sml2mqtt/__main__.py @@ -36,9 +36,14 @@ async def a_main(): device.frame_handler = device.analyze_frame # Start all devices + log.debug(f'Starting {len(ALL_DEVICES):d} device{"" if len(ALL_DEVICES) == 1 else "s":s}') await ALL_DEVICES.start() - except Exception: + except Exception as e: + log.error(f'{e.__class__.__name__} during startup: {e}') + for line in traceback.format_exc().splitlines(): + log.error(line) + await do_shutdown_async() # Keep tasks running diff --git a/src/sml2mqtt/__version__.py b/src/sml2mqtt/__version__.py index f7d2e86..27d4bb6 100644 --- a/src/sml2mqtt/__version__.py +++ b/src/sml2mqtt/__version__.py @@ -1 +1 @@ -__version__ = '3.0' +__version__ = '3.1' diff --git a/src/sml2mqtt/sml_device/sml_devices.py b/src/sml2mqtt/sml_device/sml_devices.py index 7b864b2..4ea1a1d 100644 --- a/src/sml2mqtt/sml_device/sml_devices.py +++ b/src/sml2mqtt/sml_device/sml_devices.py @@ -40,5 +40,8 @@ def check_status(self): return None + def __len__(self) -> int: + return len(self._devices) + ALL_DEVICES: Final = SmlDevices() From 1b5891706248ad0c8bfa3f44d6332c557a56a1d5 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 <10754716+spacemanspiff2007@users.noreply.github.com> Date: Mon, 5 Aug 2024 14:04:34 +0200 Subject: [PATCH 3/3] . --- tests/sml_values/test_setup_operations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sml_values/test_setup_operations.py b/tests/sml_values/test_setup_operations.py index f59bc8f..873c5f9 100644 --- a/tests/sml_values/test_setup_operations.py +++ b/tests/sml_values/test_setup_operations.py @@ -56,7 +56,7 @@ def test_field_to_init(config_model: type[BaseModel], operation: callable): annotations = inspect.get_annotations(typed_dict) for name, fwd_ref in annotations.items(): - ref_type = fwd_ref._evaluate(vars(operations_module), {}, frozenset()) + ref_type = fwd_ref._evaluate(vars(operations_module), {}, recursive_guard=frozenset()) assert name not in config_provides, config_provides config_provides[name] = ref_type else: