From c519e80731bbcdcfa3e481537f620d134948f5c9 Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Wed, 1 Apr 2026 12:39:41 +0200 Subject: [PATCH 1/8] Added __getattr__ to AttributeObject for pyright compatibility AttributeObject uses dynamic setattr() in __init__, which pyright cannot analyze statically. Adding __getattr__ with an Any return type tells pyright that dynamic attribute access is valid, while preserving correct runtime behavior (AttributeError for truly missing attributes). Signed-off-by: Lars Erik Wik --- libraries/python/cfengine_module_library.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/libraries/python/cfengine_module_library.py b/libraries/python/cfengine_module_library.py index da6a28e..3aa02eb 100644 --- a/libraries/python/cfengine_module_library.py +++ b/libraries/python/cfengine_module_library.py @@ -21,6 +21,7 @@ import traceback from copy import copy from collections import OrderedDict +from typing import Any _LOG_LEVELS = { level: idx @@ -91,6 +92,17 @@ def __init__(self, d): for key, value in d.items(): setattr(self, key, value) + # Python only calls __getattr__ as a fallback when normal attribute + # lookup (__getattribute__) has already failed, so attributes set via + # setattr() in __init__ are never affected. The -> Any return type + # tells pyright that dynamic attribute access is valid. + def __getattr__(self, name) -> Any: + raise AttributeError( + "'{}' object has no attribute '{}'".format( + self.__class__.__qualname__, name + ) + ) + def __repr__(self): return "{}({})".format( self.__class__.__qualname__, From a7628fca09f8255ce72095fc892713ce208b5c6c Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Wed, 1 Apr 2026 12:40:03 +0200 Subject: [PATCH 2/8] Fixed typo Result.NOTKEPT -> Result.NOT_KEPT in gpg module Signed-off-by: Lars Erik Wik --- examples/gpg/gpg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gpg/gpg.py b/examples/gpg/gpg.py index abe8039..8930c31 100644 --- a/examples/gpg/gpg.py +++ b/examples/gpg/gpg.py @@ -141,7 +141,7 @@ def evaluate_promise(self, promiser, attributes, metadata): f"Importing ascii key for user id '{user_id}' into gpg homedir '{promiser}'" ) if self.gpg_import_ascii(promiser, key["ascii"]): - if result != Result.NOTKEPT: + if result != Result.NOT_KEPT: result = Result.REPAIRED else: self.log_error(f"Unable to import key for user id '{user_id}'") From e6b821ac1447c3b12f1d1cc04f1087d94908a3fb Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Wed, 1 Apr 2026 12:40:32 +0200 Subject: [PATCH 3/8] Fixed evaluate_promise parameter name in ansible module Renamed safe_promiser to promiser to match the base class PromiseModule.evaluate_promise() signature. The parameter already receives the safe promiser via prepare_promiser_and_attributes(). Signed-off-by: Lars Erik Wik --- promise-types/ansible/ansible_promise.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/promise-types/ansible/ansible_promise.py b/promise-types/ansible/ansible_promise.py index fea29db..610d5ec 100644 --- a/promise-types/ansible/ansible_promise.py +++ b/promise-types/ansible/ansible_promise.py @@ -96,9 +96,9 @@ def validate_promise(self, promiser: str, attributes: Dict, metadata: Dict): return def evaluate_promise( - self, safe_promiser: str, attributes: Dict, metadata: Dict + self, promiser: str, attributes: Dict, metadata: Dict ) -> Tuple[str, List[str]]: - model = self.create_attribute_object(safe_promiser, attributes) + model = self.create_attribute_object(promiser, attributes) classes = [] result = Result.KEPT @@ -149,9 +149,7 @@ def evaluate_promise( exit_code = pbex.run() if exit_code != 0: - classes.append( - "{safe_promiser}_failed".format(safe_promiser=safe_promiser) - ) + classes.append("{safe_promiser}_failed".format(safe_promiser=promiser)) result = Result.NOT_KEPT elif callback.changed: result = Result.REPAIRED From 7f7930eba2e5a53d0c4ba131475ec499af0756d3 Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Wed, 1 Apr 2026 12:40:59 +0200 Subject: [PATCH 4/8] Fixed evaluate_promise parameter name in systemd module Renamed safe_promiser to promiser to match the base class PromiseModule.evaluate_promise() signature. The parameter already receives the safe promiser via prepare_promiser_and_attributes(). Signed-off-by: Lars Erik Wik --- promise-types/systemd/systemd.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/promise-types/systemd/systemd.py b/promise-types/systemd/systemd.py index 25f9c7d..c20ad3f 100644 --- a/promise-types/systemd/systemd.py +++ b/promise-types/systemd/systemd.py @@ -79,9 +79,9 @@ def prepare_promiser_and_attributes(self, promiser, attributes): return (safe_promiser, attributes) def evaluate_promise( - self, safe_promiser: str, attributes: Dict, metadata: Dict + self, promiser: str, attributes: Dict, metadata: Dict ) -> Tuple[str, List[str]]: - model = self.create_attribute_object(safe_promiser, attributes) + model = self.create_attribute_object(promiser, attributes) # get the status of the service try: output = self._exec_command( @@ -106,13 +106,13 @@ def evaluate_promise( self.log_error(e.stderr.strip()) return ( Result.NOT_KEPT, - ["{safe_promiser}_show_failed".format(safe_promiser=safe_promiser)], + ["{safe_promiser}_show_failed".format(safe_promiser=promiser)], ) # apply the changes if model.state == SystemdPromiseTypeStates.ABSENT.value: - return self._service_absent(model, safe_promiser, service_status) + return self._service_absent(model, promiser, service_status) else: - return self._service_present(model, safe_promiser, service_status) + return self._service_present(model, promiser, service_status) def _service_absent( self, model: AttributeObject, safe_promiser: str, service_status: dict From 78eef69bf8eb51f30829f32b3182a40d0b7e7922 Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Wed, 1 Apr 2026 12:41:15 +0200 Subject: [PATCH 5/8] Fixed create_attribute_object signature in json module Added missing promiser parameter to match the base class PromiseModule.create_attribute_object() signature. Signed-off-by: Lars Erik Wik --- promise-types/json/json_promise_type.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/promise-types/json/json_promise_type.py b/promise-types/json/json_promise_type.py index b72260c..4048b9b 100644 --- a/promise-types/json/json_promise_type.py +++ b/promise-types/json/json_promise_type.py @@ -39,7 +39,7 @@ def __init__(self, **kwargs): self.types ) # for now, the only valid attributes are the types. - def create_attribute_object(self, attributes): + def create_attribute_object(self, promiser, attributes): data = {t: None for t in self.valid_attributes} for attr, val in attributes.items(): data[attr] = val @@ -73,7 +73,7 @@ def validate_promise(self, promiser, attributes, metadata): if colon and not field: raise ValidationError("Invalid syntax: field specified but empty") - model = self.create_attribute_object(attributes) + model = self.create_attribute_object(promiser, attributes) if ( model.object and isinstance(model.object, str) @@ -113,7 +113,7 @@ def validate_promise(self, promiser, attributes, metadata): ) def evaluate_promise(self, promiser, attributes, metadata): - model = self.create_attribute_object(attributes) + model = self.create_attribute_object(promiser, attributes) filename, _, field = promiser.partition(":") if os.path.exists(filename) and not os.path.isfile(filename): From b176738294c499181fa36573ba14c4a9aec7d3ea Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Wed, 1 Apr 2026 12:41:28 +0200 Subject: [PATCH 6/8] Fixed return type annotation in iptables module Changed evaluate_command_policy return type from Result to str, since Result.KEPT and Result.REPAIRED are string class attributes, not Result instances. Signed-off-by: Lars Erik Wik --- promise-types/iptables/iptables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/promise-types/iptables/iptables.py b/promise-types/iptables/iptables.py index 2448922..ba2d86b 100644 --- a/promise-types/iptables/iptables.py +++ b/promise-types/iptables/iptables.py @@ -231,7 +231,7 @@ def evaluate_promise(self, promiser: str, attributes: Dict, metadata: Dict): return result, classes - def evaluate_command_policy(self, executable, table, chain, target) -> Result: + def evaluate_command_policy(self, executable, table, chain, target) -> str: policy_rules = self._iptables_policy_rules_of(executable, table, chain) assert len(policy_rules) == 1 and len(policy_rules[0].split()) >= 1 From f46acc7a60627a95c25962e20d07a352ff59d926 Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Wed, 1 Apr 2026 13:08:56 +0200 Subject: [PATCH 7/8] Added --diff flag to black in linting script This prints the unified diff of formatting changes on failure, making it easier to manually fix issues when black versions disagree. Signed-off-by: Lars Erik Wik --- ci/linting.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/linting.sh b/ci/linting.sh index 7db1eed..50845f8 100755 --- a/ci/linting.sh +++ b/ci/linting.sh @@ -10,7 +10,7 @@ pyright . shopt -s globstar echo "Running black" -black --check . +black --check --diff . echo "Running pyflakes" pyflakes . From d6b6342c90ff14566ae04d72b41c0f5de54f5dfa Mon Sep 17 00:00:00 2001 From: Lars Erik Wik Date: Wed, 1 Apr 2026 13:10:10 +0200 Subject: [PATCH 8/8] Manually format sshd_promise_type.py Due to conflicting versions of black. Signed-off-by: Lars Erik Wik --- promise-types/sshd/sshd_promise_type.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/promise-types/sshd/sshd_promise_type.py b/promise-types/sshd/sshd_promise_type.py index d573a09..44aa77c 100644 --- a/promise-types/sshd/sshd_promise_type.py +++ b/promise-types/sshd/sshd_promise_type.py @@ -3,8 +3,7 @@ import subprocess import tempfile -from cfengine_module_library import PromiseModule, ValidationError, Result - +from cfengine_module_library import PromiseModule, Result, ValidationError BASE_CONFIG = "/etc/ssh/sshd_config" DROP_IN_DIR = "/etc/ssh/sshd_config.d/"