From 53c48eeab424fa1b31ef0b12efa3fdebd8168c29 Mon Sep 17 00:00:00 2001 From: Chris PeBenito Date: Tue, 24 Feb 2026 14:37:20 -0500 Subject: [PATCH 1/2] Improve repr of library classes. This makes debugging for library users easier. Signed-off-by: Chris PeBenito --- setools/boolquery.py | 3 +++ setools/boundsquery.py | 5 +++++ setools/categoryquery.py | 3 +++ setools/checker/assertrbac.py | 5 +++++ setools/checker/assertte.py | 6 ++++++ setools/checker/checker.py | 3 +++ setools/checker/emptyattr.py | 3 +++ setools/checker/roexec.py | 4 ++++ setools/checker/rokmod.py | 4 ++++ setools/commonquery.py | 3 +++ setools/constraintquery.py | 8 ++++++++ setools/defaultquery.py | 5 +++++ setools/devicetreeconquery.py | 3 +++ setools/diff/difference.py | 3 +++ setools/dta.py | 5 +++++ setools/fsusequery.py | 4 ++++ setools/genfsconquery.py | 5 +++++ setools/ibendportconquery.py | 4 ++++ setools/ibpkeyconquery.py | 6 ++++++ setools/infoflow.py | 6 ++++++ setools/initsidquery.py | 3 +++ setools/iomemconquery.py | 6 ++++++ setools/ioportconquery.py | 6 ++++++ setools/mixins.py | 24 ++++++++++++++++++++++++ setools/mlsrulequery.py | 11 +++++++++++ setools/netifconquery.py | 3 +++ setools/nodeconquery.py | 5 +++++ setools/objclassquery.py | 5 +++++ setools/pcideviceconquery.py | 3 +++ setools/pirqconquery.py | 3 +++ setools/polcapquery.py | 3 +++ setools/policyrep.pyi | 12 ++++++------ setools/portconquery.py | 6 ++++++ setools/query.py | 11 ++++++++++- setools/rbacrulequery.py | 10 ++++++++-- setools/rolequery.py | 5 +++++ setools/roletypesquery.py | 3 +++ setools/sensitivityquery.py | 6 ++++++ setools/terulequery.py | 11 +++++++++++ setools/typeattrquery.py | 5 +++++ setools/typequery.py | 6 ++++++ setools/userquery.py | 9 +++++++++ 42 files changed, 235 insertions(+), 9 deletions(-) diff --git a/setools/boolquery.py b/setools/boolquery.py index 49e5388c..7592d751 100644 --- a/setools/boolquery.py +++ b/setools/boolquery.py @@ -38,6 +38,9 @@ def default(self, value) -> None: else: self._default = bool(value) + def _build_repr_args(self) -> list[str]: + return [f"default={self.default!r}"] + self._build_name_repr_args() + def results(self) -> Iterable[policyrep.Boolean]: """Generator which yields all Booleans matching the criteria.""" self.log.info(f"Generating Boolean results from {self.policy}") diff --git a/setools/boundsquery.py b/setools/boundsquery.py index a21f6b5a..fae8ea50 100644 --- a/setools/boundsquery.py +++ b/setools/boundsquery.py @@ -29,6 +29,11 @@ class BoundsQuery(query.PolicyQuery): child = CriteriaDescriptor[policyrep.Type]("child_regex") child_regex: bool = False + def _build_repr_args(self) -> list[str]: + return [f"ruletype={self.ruletype!r}", f"parent={self.parent!r}", + f"parent_regex={self.parent_regex!r}", f"child={self.child!r}", + f"child_regex={self.child_regex!r}"] + def results(self) -> Iterable[policyrep.Bounds]: """Generator which yields all matching *bounds statements.""" self.log.info(f"Generating bounds results from {self.policy}") diff --git a/setools/categoryquery.py b/setools/categoryquery.py index a82a1777..076684c0 100644 --- a/setools/categoryquery.py +++ b/setools/categoryquery.py @@ -27,6 +27,9 @@ class CategoryQuery(mixins.MatchAlias, mixins.MatchName, query.PolicyQuery): will be used on the alias names. """ + def _build_repr_args(self) -> list[str]: + return self._build_name_repr_args() + self._build_alias_repr_args() + def results(self) -> Iterable[policyrep.Category]: """Generator which yields all matching categories.""" self.log.info(f"Generating category results from {self.policy}") diff --git a/setools/checker/assertrbac.py b/setools/checker/assertrbac.py index cee932c8..8bbb0894 100644 --- a/setools/checker/assertrbac.py +++ b/setools/checker/assertrbac.py @@ -63,6 +63,11 @@ def __init__(self, policy: policyrep.SELinuxPolicy, checkname: str, self.log.info("Overlap in expect_target and exempt_target: " f"{', '.join(i.name for i in target_exempt_expect_overlap)}") + def __repr__(self) -> str: + return (f"{self.__class__.__name__}(source={self.source}, target={self.target}, " + f"exempt_source={self.exempt_source}, exempt_target={self.exempt_target}, " + f"expect_source={self.expect_source}, expect_target={self.expect_target})") + def run(self) -> list[policyrep.AnyRBACRule | str]: assert any((self.source, self.target)), "AssertRBAC no options set, this is a bug." diff --git a/setools/checker/assertte.py b/setools/checker/assertte.py index 668c6c28..91d6c6fb 100644 --- a/setools/checker/assertte.py +++ b/setools/checker/assertte.py @@ -77,6 +77,12 @@ def __init__(self, policy: policyrep.SELinuxPolicy, checkname: str, self.log.info("Overlap in expect_target and exempt_target: " f"{', '.join(i.name for i in target_exempt_expect_overlap)}") + def __repr__(self) -> str: + return (f"{self.__class__.__name__}(source={self.source}, target={self.target}, " + f"tclass={self.tclass}, perms={self.perms}, " + f"exempt_source={self.exempt_source}, exempt_target={self.exempt_target}, " + f"expect_source={self.expect_source}, expect_target={self.expect_target})") + def run(self) -> list[policyrep.AnyTERule | str]: assert any((self.source, self.target, self.tclass, self.perms)), \ "AssertTe no options set, this is a bug." diff --git a/setools/checker/checker.py b/setools/checker/checker.py index 3a0bc418..3709b297 100644 --- a/setools/checker/checker.py +++ b/setools/checker/checker.py @@ -75,6 +75,9 @@ def config(self, configpath: str) -> None: self.checks = checks self._config = config + def __repr__(self) -> str: + return f"<{self.__class__.__name__}({self.policy!r}, {self.config!r})>" + def run(self, output: typing.TextIO = sys.stdout) -> int: """Run all configured checks and print report to the file-like output.""" failures = 0 diff --git a/setools/checker/emptyattr.py b/setools/checker/emptyattr.py index 3fb8a0bd..3c7fc15e 100644 --- a/setools/checker/emptyattr.py +++ b/setools/checker/emptyattr.py @@ -73,6 +73,9 @@ def missing_ok(self, value) -> None: else: self._pass_by_missing = False + def __repr__(self) -> str: + return (f"{self.__class__.__name__}(attr={self.attr}, missing_ok={self.missing_ok})") + def run(self) -> list[policyrep.Type]: self.log.info(f"Checking type attribute {self.attr} is empty.") diff --git a/setools/checker/roexec.py b/setools/checker/roexec.py index f43caa99..5500f41c 100644 --- a/setools/checker/roexec.py +++ b/setools/checker/roexec.py @@ -41,6 +41,10 @@ def __init__(self, policy: policyrep.SELinuxPolicy, checkname: str, self.exempt_file = config.get(EXEMPT_FILE) self.exempt_exec_domain = config.get(EXEMPT_EXEC) + def __repr__(self) -> str: + return (f"{self.__class__.__name__}(exempt_write_domain={self.exempt_write_domain}, " + f"exempt_exec_domain={self.exempt_exec_domain}, exempt_file={self.exempt_file})") + def _collect_executables(self) -> defaultdict[policyrep.Type, set[policyrep.AVRule]]: self.log.debug("Collecting list of executable file types.") self.log.debug(f"{self.exempt_exec_domain=}") diff --git a/setools/checker/rokmod.py b/setools/checker/rokmod.py index 183f4990..81e19ed5 100644 --- a/setools/checker/rokmod.py +++ b/setools/checker/rokmod.py @@ -41,6 +41,10 @@ def __init__(self, policy: policyrep.SELinuxPolicy, checkname: str, self.exempt_file = config.get(EXEMPT_FILE) self.exempt_load_domain = config.get(EXEMPT_LOAD) + def __repr__(self) -> str: + return (f"{self.__class__.__name__}(exempt_write_domain={self.exempt_write_domain}, " + f"exempt_load_domain={self.exempt_load_domain}, exempt_file={self.exempt_file})") + def _collect_kernel_mods(self) -> defaultdict[policyrep.Type, set[policyrep.AVRule]]: self.log.debug("Collecting list of kernel module types.") self.log.debug(f"{self.exempt_load_domain=}") diff --git a/setools/commonquery.py b/setools/commonquery.py index ca145c87..69bce12a 100644 --- a/setools/commonquery.py +++ b/setools/commonquery.py @@ -31,6 +31,9 @@ class CommonQuery(mixins.MatchPermission, mixins.MatchName, query.PolicyQuery): on the permission names instead of set logic. """ + def _build_repr_args(self) -> list[str]: + return self._build_name_repr_args() + self._build_perms_repr_args() + def results(self) -> Iterable[policyrep.Common]: """Generator which yields all matching commons.""" self.log.info(f"Generating common results from {self.policy}") diff --git a/setools/constraintquery.py b/setools/constraintquery.py index 21a7fda1..f8beead2 100644 --- a/setools/constraintquery.py +++ b/setools/constraintquery.py @@ -60,6 +60,14 @@ class ConstraintQuery(mixins.MatchObjClass, mixins.MatchPermission, query.Policy type_regex: bool = False type_indirect: bool = True + def _build_repr_args(self) -> list[str]: + return [f"user={self.user!r}", f"user_regex={self.user_regex!r}", f"role={self.role!r}", + f"role_regex={self.role_regex!r}", f"role_indirect={self.role_indirect!r}", + f"type_={self.type_!r}", f"type_regex={self.type_regex!r}", + f"type_indirect={self.type_indirect!r}"] \ + + self._build_object_class_repr_args() \ + + self._build_perms_repr_args() + def _match_expr(self, expr: frozenset[policyrep.User] | frozenset[policyrep.Role] | frozenset[policyrep.Type], criteria, indirect: bool, regex: bool) -> bool: """ diff --git a/setools/defaultquery.py b/setools/defaultquery.py index 08d6551e..da8555b1 100644 --- a/setools/defaultquery.py +++ b/setools/defaultquery.py @@ -36,6 +36,11 @@ class DefaultQuery(mixins.MatchObjClass, query.PolicyQuery): default_range = CriteriaDescriptor[policyrep.DefaultRangeValue]( enum_class=policyrep.DefaultRangeValue) + def _build_repr_args(self) -> list[str]: + return [f"ruletype={self.ruletype!r}", f"default={self.default!r}", + f"default_range={self.default_range!r}"] \ + + self._build_object_class_repr_args() + def results(self) -> Iterable[policyrep.AnyDefault]: """Generator which yields all matching default_* statements.""" self.log.info(f"Generating default_* results from {self.policy}") diff --git a/setools/devicetreeconquery.py b/setools/devicetreeconquery.py index c1beb760..58fe1bc0 100644 --- a/setools/devicetreeconquery.py +++ b/setools/devicetreeconquery.py @@ -48,6 +48,9 @@ class DevicetreeconQuery(mixins.MatchContext, query.PolicyQuery): path: str | None = None + def _build_repr_args(self) -> list[str]: + return [f"path={self.path!r}"] + self._build_context_repr_args() + def results(self) -> Iterable[policyrep.Devicetreecon]: """Generator which yields all matching devicetreecons.""" self.log.info(f"Generating results from {self.policy}") diff --git a/setools/diff/difference.py b/setools/diff/difference.py index 12ecc4c0..cd9700ba 100644 --- a/setools/diff/difference.py +++ b/setools/diff/difference.py @@ -46,6 +46,9 @@ def right_policy(self, policy): # # Internal functions # + def __repr__(self) -> str: + return f"<{self.__class__.__name__}({self.left_policy!r}, {self.right_policy!r})>" + def _reset_diff(self) -> None: """Reset diff results on policy changes.""" raise NotImplementedError diff --git a/setools/dta.py b/setools/dta.py index 31475268..bf0f1c29 100644 --- a/setools/dta.py +++ b/setools/dta.py @@ -202,6 +202,11 @@ def exclude(self, types: Iterable[policyrep.Type | str] | None) -> None: self.rebuildsubgraph = True + def _build_repr_args(self) -> list[str]: + return [f"source={self.source!r}", f"target={self.target!r}", + f"mode={self.mode!r}", f"depth_limit={self.depth_limit!r}", + f"exclude={self.exclude!r}", f"reverse={self.reverse!r}"] + def results(self) -> Iterable[DTAPath] | Iterable[DomainTransition]: if self.rebuildsubgraph: self._build_subgraph() diff --git a/setools/fsusequery.py b/setools/fsusequery.py index bc0a9b0a..c3254676 100644 --- a/setools/fsusequery.py +++ b/setools/fsusequery.py @@ -50,6 +50,10 @@ class FSUseQuery(mixins.MatchContext, query.PolicyQuery): fs = CriteriaDescriptor[str]("fs_regex") fs_regex: bool = False + def _build_repr_args(self) -> list[str]: + return [f"ruletype={self.ruletype!r}", f"fs={self.fs!r}", f"fs_regex={self.fs_regex!r}"] \ + + self._build_context_repr_args() + def results(self) -> Iterable[policyrep.FSUse]: """Generator which yields all matching fs_use_* statements.""" self.log.info(f"Generating fs_use_* results from {self.policy}") diff --git a/setools/genfsconquery.py b/setools/genfsconquery.py index 7e51d7b4..9b4c7111 100644 --- a/setools/genfsconquery.py +++ b/setools/genfsconquery.py @@ -54,6 +54,11 @@ class GenfsconQuery(mixins.MatchContext, query.PolicyQuery): path = CriteriaDescriptor[str]("path_regex") path_regex: bool = False + def _build_repr_args(self) -> list[str]: + return [f"fs={self.fs!r}", f"fs_regex={self.fs_regex!r}", f"path={self.path!r}", + f"path_regex={self.path_regex!r}", f"filetype={self.filetype!r}"] \ + + self._build_context_repr_args() + def results(self) -> Iterable[policyrep.Genfscon]: """Generator which yields all matching genfscons.""" self.log.info(f"Generating genfscon results from {self.policy}") diff --git a/setools/ibendportconquery.py b/setools/ibendportconquery.py index d8bc1345..757bad97 100644 --- a/setools/ibendportconquery.py +++ b/setools/ibendportconquery.py @@ -62,6 +62,10 @@ def port(self, value: int | None) -> None: else: self._port = None + def _build_repr_args(self) -> list[str]: + return [f"port={self.port!r}"] + self._build_name_repr_args() \ + + self._build_context_repr_args() + def results(self) -> Iterable[policyrep.Ibendportcon]: """Generator which yields all matching ibendportcons.""" self.log.info(f"Generating ibendportcon results from {self.policy}") diff --git a/setools/ibpkeyconquery.py b/setools/ibpkeyconquery.py index 0064521d..38e6fd54 100644 --- a/setools/ibpkeyconquery.py +++ b/setools/ibpkeyconquery.py @@ -82,6 +82,12 @@ def subnet_prefix(self, value: str | IPv6Address | None) -> None: else: self._subnet_prefix = None + def _build_repr_args(self) -> list[str]: + return [f"subnet_prefix={self.subnet_prefix!r}", f"pkeys={self.pkeys!r}", + f"pkeys_subset={self.pkeys_subset!r}", f"pkeys_overlap={self.pkeys_overlap!r}", + f"pkeys_superset={self.pkeys_superset!r}", f"pkeys_proper={self.pkeys_proper!r}"] \ + + self._build_context_repr_args() + def results(self) -> Iterable[policyrep.Ibpkeycon]: """Generator which yields all matching ibpkeycons.""" self.log.info(f"Generating ibpkeycon results from {self.policy}") diff --git a/setools/infoflow.py b/setools/infoflow.py index 20b04763..ca8d718a 100644 --- a/setools/infoflow.py +++ b/setools/infoflow.py @@ -143,6 +143,12 @@ def exclude(self, types: Iterable[policyrep.Type | str] | None) -> None: self.rebuildsubgraph = True + def _build_repr_args(self) -> list[str]: + return [repr(self.perm_map), + f"source={self.source!r}", f"target={self.target!r}", f"mode={self.mode!r}", + f"min_weight={self.min_weight!r}", f"exclude={self.exclude!r}", + f"booleans={self.booleans!r}", f"depth_limit={self.depth_limit!r}"] + def results(self) -> Iterable[InfoFlowPath] | Iterable["InfoFlowStep"]: if self.rebuildsubgraph: self._build_subgraph() diff --git a/setools/initsidquery.py b/setools/initsidquery.py index 05e1280c..354490e2 100644 --- a/setools/initsidquery.py +++ b/setools/initsidquery.py @@ -44,6 +44,9 @@ class InitialSIDQuery(mixins.MatchName, mixins.MatchContext, query.PolicyQuery): required_platform = policyrep.PolicyTarget.selinux + def _build_repr_args(self) -> list[str]: + return self._build_name_repr_args() + self._build_context_repr_args() + def results(self) -> Iterable[policyrep.InitialSID]: """Generator which yields all matching initial SIDs.""" self.log.info(f"Generating initial SID results from {self.policy}") diff --git a/setools/iomemconquery.py b/setools/iomemconquery.py index fb735d0b..9cbe81a2 100644 --- a/setools/iomemconquery.py +++ b/setools/iomemconquery.py @@ -72,6 +72,12 @@ def addr(self, value: policyrep.IomemconRange | tuple[int, int] | None) -> None: else: self._addr = policyrep.IomemconRange(*value) if value else None + def _build_repr_args(self) -> list[str]: + return [f"addr={self.addr!r}", f"addr_subset={self.addr_subset!r}", + f"addr_overlap={self.addr_overlap!r}", f"addr_superset={self.addr_superset!r}", + f"addr_proper={self.addr_proper!r}"] \ + + self._build_context_repr_args() + def results(self) -> Iterable[policyrep.Iomemcon]: """Generator which yields all matching iomemcons.""" self.log.info(f"Generating results from {self.policy}") diff --git a/setools/ioportconquery.py b/setools/ioportconquery.py index fef8ec1d..3138e6dd 100644 --- a/setools/ioportconquery.py +++ b/setools/ioportconquery.py @@ -72,6 +72,12 @@ def ports(self, value: policyrep.IoportconRange | tuple[int, int] | None) -> Non else: self._ports = policyrep.IoportconRange(*value) if value else None + def _build_repr_args(self) -> list[str]: + return [f"ports={self.ports!r}", f"ports_subset={self.ports_subset!r}", + f"ports_overlap={self.ports_overlap!r}", f"ports_superset={self.ports_superset!r}", + f"ports_proper={self.ports_proper!r}"] \ + + self._build_context_repr_args() + def results(self) -> Iterable[policyrep.Ioportcon]: """Generator which yields all matching ioportcons.""" self.log.info(f"Generating results from {self.policy}") diff --git a/setools/mixins.py b/setools/mixins.py index cdef6b99..b377e321 100644 --- a/setools/mixins.py +++ b/setools/mixins.py @@ -5,6 +5,7 @@ # # pylint: disable=attribute-defined-outside-init,no-member from logging import Logger +import re from typing import Any from .descriptors import CriteriaDescriptor, CriteriaSetDescriptor, CriteriaPermissionSetDescriptor @@ -18,6 +19,9 @@ class MatchAlias: alias = CriteriaDescriptor[str]("alias_regex") alias_regex: bool = False + def _build_alias_repr_args(self) -> list[str]: + return [f"alias={self.alias!r}", f"alias_regex={self.alias_regex!r}"] + def _match_alias_debug(self, log: Logger) -> None: """Emit log debugging info for alias matching.""" log.debug(f"{self.alias=}, {self.alias_regex=}") @@ -76,6 +80,14 @@ class MatchContext: range_superset: bool = False range_proper: bool = False + def _build_context_repr_args(self) -> list[str]: + return [f"user={self.user!r}", f"user_regex={self.user_regex!r}", + f"role={self.role!r}", f"role_regex={self.role_regex!r}", + f"type_={self.type_!r}", f"type_regex={self.type_regex!r}", + f"range_={self.range_!r}", f"range_subset={self.range_subset!r}", + f"range_overlap={self.range_overlap!r}", f"range_superset={self.range_superset!r}", + f"range_proper={self.range_proper!r}"] + def _match_context_debug(self, log: Logger): """Emit log debugging info for context matching.""" log.debug(f"{self.user=}, {self.user_regex=}") @@ -131,6 +143,10 @@ class MatchName: name_regex: bool = False alias_deref: bool = False + def _build_name_repr_args(self) -> list[str]: + return [f"name={self.name!r}", f"name_regex={self.name_regex!r}", + f"alias_deref={self.alias_deref!r}"] + def _match_name_debug(self, log: Logger) -> None: """Log debugging messages for name matching.""" log.debug(f"{self.name=}, {self.name_regex=}, {self.alias_deref=}") @@ -155,6 +171,9 @@ class MatchObjClass: tclass = CriteriaSetDescriptor[policyrep.ObjClass]("tclass_regex", "lookup_class") tclass_regex: bool = False + def _build_object_class_repr_args(self) -> list[str]: + return [f"tclass={self.tclass!r}", f"tclass_regex={self.tclass_regex!r}"] + def _match_object_class_debug(self, log: Logger) -> None: """Emit log debugging info for permission matching.""" log.debug(f"{self.tclass=}, {self.tclass_regex=}") @@ -171,6 +190,7 @@ def _match_object_class(self, obj): # if there is no criteria, everything matches. return True elif self.tclass_regex: + assert isinstance(self.tclass, re.Pattern) return bool(self.tclass.search(str(obj.tclass))) else: return obj.tclass in self.tclass @@ -185,6 +205,10 @@ class MatchPermission: perms_regex: bool = False perms_subset: bool = False + def _build_perms_repr_args(self) -> list[str]: + return [f"perms={self.perms!r}", f"perms_equal={self.perms_equal!r}", + f"perms_regex={self.perms_regex!r}", f"perms_subset={self.perms_subset!r}"] + def _match_perms_debug(self, log: Logger): """Emit log debugging info for permission matching.""" log.debug(f"{self.perms=}, {self.perms_regex=}, {self.perms_equal=}, " diff --git a/setools/mlsrulequery.py b/setools/mlsrulequery.py index 5c34432d..4d254f8d 100644 --- a/setools/mlsrulequery.py +++ b/setools/mlsrulequery.py @@ -47,6 +47,17 @@ class MLSRuleQuery(mixins.MatchObjClass, query.PolicyQuery): default_superset: bool = False default_proper: bool = False + def _build_repr_args(self) -> list[str]: + return [f"ruletype={self.ruletype!r}", f"source={self.source!r}", + f"source_regex={self.source_regex!r}", f"source_indirect={self.source_indirect!r}", + f"target={self.target!r}", f"target_regex={self.target_regex!r}", + f"target_indirect={self.target_indirect!r}", f"default={self.default!r}", + f"default_overlap={self.default_overlap!r}", + f"default_subset={self.default_subset!r}", + f"default_superset={self.default_superset!r}", + f"default_proper={self.default_proper!r}"] \ + + self._build_object_class_repr_args() + def results(self) -> Iterable[policyrep.MLSRule]: """Generator which yields all matching MLS rules.""" self.log.info(f"Generating MLS rule results from {self.policy}") diff --git a/setools/netifconquery.py b/setools/netifconquery.py index a07828aa..e71ad4b2 100644 --- a/setools/netifconquery.py +++ b/setools/netifconquery.py @@ -44,6 +44,9 @@ class NetifconQuery(mixins.MatchContext, mixins.MatchName, query.PolicyQuery): required_platform = policyrep.PolicyTarget.selinux + def _build_repr_args(self) -> list[str]: + return self._build_name_repr_args() + self._build_context_repr_args() + def results(self) -> Iterable[policyrep.Netifcon]: """Generator which yields all matching netifcons.""" self.log.info(f"Generating netifcon results from {self.policy}") diff --git a/setools/nodeconquery.py b/setools/nodeconquery.py index 73ecd451..c533947f 100644 --- a/setools/nodeconquery.py +++ b/setools/nodeconquery.py @@ -78,6 +78,11 @@ def network(self, value: AnyIPNetwork | str | None) -> None: else: self._network = None + def _build_repr_args(self) -> list[str]: + return [f"network={self.network!r}", f"network_overlap={self.network_overlap!r}", + f"ip_version={self.ip_version!r}"] \ + + self._build_context_repr_args() + def results(self) -> Iterable[policyrep.Nodecon]: """Generator which yields all matching nodecons.""" self.log.info(f"Generating nodecon results from {self.policy}") diff --git a/setools/objclassquery.py b/setools/objclassquery.py index 946383bf..f26725cf 100644 --- a/setools/objclassquery.py +++ b/setools/objclassquery.py @@ -48,6 +48,11 @@ class ObjClassQuery(mixins.MatchName, query.PolicyQuery): perms_indirect: bool = True perms_regex: bool = False + def _build_repr_args(self) -> list[str]: + return [f"{self.common=}", f"{self.common_regex=}", f"{self.perms=}", + f"{self.perms_equal=}", f"{self.perms_indirect=}", f"{self.perms_regex=}"] \ + + self._build_name_repr_args() + def results(self) -> Iterable[policyrep.ObjClass]: """Generator which yields all matching object classes.""" self.log.info(f"Generating object class results from {self.policy}") diff --git a/setools/pcideviceconquery.py b/setools/pcideviceconquery.py index 516d549a..d872c4d5 100644 --- a/setools/pcideviceconquery.py +++ b/setools/pcideviceconquery.py @@ -62,6 +62,9 @@ def device(self, value: int | None) -> None: else: self._device = None + def _build_repr_args(self) -> list[str]: + return [f"device={self.device!r}"] + self._build_context_repr_args() + def results(self) -> Iterable[policyrep.Pcidevicecon]: """Generator which yields all matching pcidevicecons.""" self.log.info(f"Generating results from {self.policy}") diff --git a/setools/pirqconquery.py b/setools/pirqconquery.py index 1af68f0f..37f5de0a 100644 --- a/setools/pirqconquery.py +++ b/setools/pirqconquery.py @@ -62,6 +62,9 @@ def irq(self, value: int | None) -> None: else: self._irq = None + def _build_repr_args(self) -> list[str]: + return [f"irq={self.irq!r}"] + self._build_context_repr_args() + def results(self) -> Iterable[policyrep.Pirqcon]: """Generator which yields all matching pirqcons.""" self.log.info(f"Generating results from {self.policy}") diff --git a/setools/polcapquery.py b/setools/polcapquery.py index 8d2e6ea5..335f7995 100644 --- a/setools/polcapquery.py +++ b/setools/polcapquery.py @@ -24,6 +24,9 @@ class PolCapQuery(mixins.MatchName, query.PolicyQuery): be used for matching the name. """ + def _build_repr_args(self) -> list[str]: + return self._build_name_repr_args() + def results(self) -> Iterable[policyrep.PolicyCapability]: """Generator which yields all matching policy capabilities.""" self.log.info(f"Generating policy capability results from {self.policy}") diff --git a/setools/policyrep.pyi b/setools/policyrep.pyi index 97a00f8e..9377a3f6 100644 --- a/setools/policyrep.pyi +++ b/setools/policyrep.pyi @@ -3,16 +3,16 @@ from collections.abc import Callable, Iterable, Iterator from dataclasses import dataclass -from typing import Any, NoReturn +from typing import Any, NoReturn, TypeAlias import enum import ipaddress -AnyConstraint = "Constraint" | "Validatetrans" -AnyDefault = "Default" | "DefaultRange" -AnyRBACRule = "RoleAllow" | "RoleTransition" -AnyTERule = "AVRule" | "AVRuleXperm" | "TERule" | "FileNameTERule" -TypeOrAttr = "Type" | "TypeAttribute" +AnyConstraint: TypeAlias = "Constraint" | "Validatetrans" +AnyDefault: TypeAlias = "Default" | "DefaultRange" +AnyRBACRule: TypeAlias = "RoleAllow" | "RoleTransition" +AnyTERule: TypeAlias = "AVRule" | "AVRuleXperm" | "TERule" | "FileNameTERule" +TypeOrAttr: TypeAlias = "Type" | "TypeAttribute" def lookup_boolean_name_sub(name: str) -> str: ... diff --git a/setools/portconquery.py b/setools/portconquery.py index b1c9ab16..1ea376a0 100644 --- a/setools/portconquery.py +++ b/setools/portconquery.py @@ -87,6 +87,12 @@ def protocol(self, value: policyrep.PortconProtocol | str | None) -> None: else: self._protocol = None + def _build_repr_args(self) -> list[str]: + return [f"protocol={self.protocol!r}", f"ports={self.ports!r}", + f"ports_subset={self.ports_subset!r}", f"ports_overlap={self.ports_overlap!r}", + f"ports_superset={self.ports_superset!r}", f"ports_proper={self.ports_proper!r}"] \ + + self._build_context_repr_args() + def results(self) -> Iterable[policyrep.Portcon]: """Generator which yields all matching portcons.""" self.log.info(f"Generating portcon results from {self.policy}") diff --git a/setools/query.py b/setools/query.py index 58fc9dc1..e8d553cc 100644 --- a/setools/query.py +++ b/setools/query.py @@ -10,6 +10,7 @@ from . import exception if typing.TYPE_CHECKING: + from collections.abc import Iterable from networkx import DiGraph from .policyrep import PolicyTarget, SELinuxPolicy @@ -53,7 +54,15 @@ def policy(self, value: "SELinuxPolicy") -> None: self._policy = value @abstractmethod - def results(self) -> typing.Iterable: + def _build_repr_args(self) -> list[str]: + """Build the argument list for the __repr__ method.""" + + def __repr__(self) -> str: + args: str = ", ".join(self._build_repr_args()) + return f"<{self.__class__.__name__}({repr(self.policy)}, {args})>" + + @abstractmethod + def results(self) -> "Iterable": """ Generator which returns the matches for the query. This method should be overridden by subclasses. diff --git a/setools/rbacrulequery.py b/setools/rbacrulequery.py index 2e89f1d7..1eb000ae 100644 --- a/setools/rbacrulequery.py +++ b/setools/rbacrulequery.py @@ -49,8 +49,6 @@ class RBACRuleQuery(mixins.MatchObjClass, query.PolicyQuery): _target: re.Pattern[str] | policyrep.Role | policyrep.TypeOrAttr | None = None target_regex: bool = False target_indirect: bool = True - tclass = CriteriaSetDescriptor[policyrep.ObjClass]("tclass_regex", "lookup_class") - tclass_regex: bool = False default = CriteriaDescriptor[policyrep.Role]("default_regex", "lookup_role") default_regex: bool = False @@ -72,6 +70,14 @@ def target(self, value: str | policyrep.Role | policyrep.TypeOrAttr | None) -> N self._target = self.policy.lookup_role( typing.cast(str | policyrep.Role, value)) + def _build_repr_args(self) -> list[str]: + return [f"ruletype={self.ruletype!r}", f"source={self.source!r}", + f"source_indirect={self.source_indirect!r}", f"source_regex={self.source_regex!r}", + f"target={self.target!r}", f"target_indirect={self.target_indirect!r}", + f"target_regex={self.target_regex!r}", f"default={self.default!r}", + f"default_regex={self.default_regex!r}"] \ + + self._build_object_class_repr_args() + def results(self) -> Iterable[policyrep.AnyRBACRule]: """Generator which yields all matching RBAC rules.""" self.log.info(f"Generating RBAC rule results from {self.policy}") diff --git a/setools/rolequery.py b/setools/rolequery.py index 1d23bd04..7b258b4a 100644 --- a/setools/rolequery.py +++ b/setools/rolequery.py @@ -37,6 +37,11 @@ class RoleQuery(mixins.MatchName, query.PolicyQuery): types_equal: bool = False types_regex: bool = False + def _build_repr_args(self) -> list[str]: + return [f"types={self.types!r}", f"types_equal={self.types_equal!r}", + f"types_regex={self.types_regex!r}"] \ + + self._build_name_repr_args() + def results(self) -> Iterable[policyrep.Role]: """Generator which yields all matching roles.""" self.log.info(f"Generating role results from {self.policy}") diff --git a/setools/roletypesquery.py b/setools/roletypesquery.py index 7ef9b3ef..71cdcf77 100644 --- a/setools/roletypesquery.py +++ b/setools/roletypesquery.py @@ -24,6 +24,9 @@ class RoleTypesQuery(mixins.MatchName, query.PolicyQuery): will be used on the type names. """ + def _build_repr_args(self) -> list[str]: + return self._build_name_repr_args() + def results(self) -> Iterable[policyrep.Role]: """Generator which yields all matching roles.""" self.log.info(f"Generating role-types results from {self.policy}") diff --git a/setools/sensitivityquery.py b/setools/sensitivityquery.py index 0d90562f..b21a7927 100644 --- a/setools/sensitivityquery.py +++ b/setools/sensitivityquery.py @@ -36,6 +36,12 @@ class SensitivityQuery(MatchAlias, MatchName, PolicyQuery): sens_dom: bool = False sens_domby: bool = False + def _build_repr_args(self) -> list[str]: + return [f"sens={self.sens!r}", f"sens_dom={self.sens_dom!r}", + f"sens_domby={self.sens_domby!r}"] \ + + self._build_name_repr_args() \ + + self._build_alias_repr_args() + def results(self) -> Iterable[policyrep.Sensitivity]: """Generator which yields all matching sensitivities.""" self.log.info(f"Generating sensitivity results from {self.policy}") diff --git a/setools/terulequery.py b/setools/terulequery.py index 866495c3..a969a246 100644 --- a/setools/terulequery.py +++ b/setools/terulequery.py @@ -108,6 +108,17 @@ def xperms(self, value: Iterable[tuple[int, int]] | None) -> None: else: self._xperms = None + def _build_repr_args(self) -> list[str]: + return [f"ruletype={self.ruletype!r}", f"source={self.source!r}", + f"source_indirect={self.source_indirect!r}", f"source_regex={self.source_regex!r}", + f"target={self.target!r}", f"target_indirect={self.target_indirect!r}", + f"target_regex={self.target_regex!r}", f"default={self.default!r}", + f"default_regex={self.default_regex!r}", f"boolean={self.boolean!r}", + f"boolean_regex={self.boolean_regex!r}", f"boolean_equal={self.boolean_equal!r}", + f"xperms={self.xperms!r}", f"xperms_equal={self.xperms_equal!r}"] \ + + self._build_object_class_repr_args() \ + + self._build_perms_repr_args() + def results(self) -> Iterable[policyrep.AnyTERule]: """Generator which yields all matching TE rules.""" self.log.info(f"Generating TE rule results from {self.policy}") diff --git a/setools/typeattrquery.py b/setools/typeattrquery.py index fbcd7fef..9f1a5a33 100644 --- a/setools/typeattrquery.py +++ b/setools/typeattrquery.py @@ -36,6 +36,11 @@ class TypeAttributeQuery(MatchName, PolicyQuery): types_equal: bool = False types_regex: bool = False + def _build_repr_args(self) -> list[str]: + return [f"types={self.types!r}", f"types_equal={self.types_equal!r}", + f"types_regex={self.types_regex!r}"] \ + + self._build_name_repr_args() + def results(self) -> Iterable[policyrep.TypeAttribute]: """Generator which yields all matching types.""" self.log.info(f"Generating type attribute results from {self.policy}") diff --git a/setools/typequery.py b/setools/typequery.py index e53163c4..45ac97b6 100644 --- a/setools/typequery.py +++ b/setools/typequery.py @@ -51,6 +51,12 @@ def permissive(self, value) -> None: else: self._permissive = bool(value) + def _build_repr_args(self) -> list[str]: + return [f"attrs={self.attrs!r}", f"attrs_equal={self.attrs_equal!r}", + f"attrs_regex={self.attrs_regex!r}", f"permissive={self.permissive!r}"] \ + + self._build_name_repr_args() \ + + self._build_alias_repr_args() + def results(self) -> Iterable[policyrep.Type]: """Generator which yields all matching types.""" self.log.info(f"Generating type results from {self.policy}") diff --git a/setools/userquery.py b/setools/userquery.py index dc3ff2a1..cb2d173a 100644 --- a/setools/userquery.py +++ b/setools/userquery.py @@ -62,6 +62,15 @@ class UserQuery(mixins.MatchName, query.PolicyQuery): roles_equal: bool = False roles_regex: bool = False + def _build_repr_args(self) -> list[str]: + return [f"level={self.level!r}", f"level_dom={self.level_dom!r}", + f"level_domby={self.level_domby!r}", f"level_incomp={self.level_incomp!r}", + f"range_={self.range_!r}", f"range_subset={self.range_subset!r}", + f"range_overlap={self.range_overlap!r}", f"range_superset={self.range_superset!r}", + f"range_proper={self.range_proper!r}", f"roles={self.roles!r}", + f"roles_equal={self.roles_equal!r}", f"roles_regex={self.roles_regex!r}"] \ + + self._build_name_repr_args() + def results(self) -> Iterable[policyrep.User]: """Generator which yields all matching users.""" self.log.info(f"Generating user results from {self.policy}") From 2d64ca76324e59dd54ff7173219412199847722c Mon Sep 17 00:00:00 2001 From: Chris PeBenito Date: Tue, 24 Feb 2026 14:57:02 -0500 Subject: [PATCH 2/2] tox.ini: Fix toml extra specification for coverage target. Signed-off-by: Chris PeBenito --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index b2a21d6b..72ee6753 100644 --- a/tox.ini +++ b/tox.ini @@ -15,8 +15,7 @@ commands = pycodestyle setools/ setoolsgui/ tests/ seinfo seinfoflow sedt #setenv = SETOOLS_COVERAGE = 1 passenv = {[testenv]passenv} deps = {[testenv]deps} - coverage>=6.0 -extras = toml + coverage[toml]>=6.0 commands_pre = coverage --version coverage erase {[testenv]commands_pre}