# SPDX-License-Identifier: GPLv3-or-later
# Copyright © 2025 pygaindalf Rui Pinheiro
import re
from abc import ABCMeta
from typing import ClassVar, Protocol, override, runtime_checkable
from ...util.helpers import type_hints
# MARK: shorten_name
[docs]
def shorten_name(name: str) -> str:
# Assume camelcase
res = re.sub("[^A-Z0-9]", "", name)
if res:
return res
# Otherwise, do nothing
return name
# MARK: Protocols
[docs]
@runtime_checkable
class NamedProtocol(Protocol):
@property
def instance_name(self) -> str | None: ...
[docs]
@runtime_checkable
class FinalNamedProtocol(NamedProtocol, Protocol):
@property
def final_instance_name(self) -> str: ...
[docs]
@runtime_checkable
class NamedMutableProtocol(NamedProtocol, Protocol):
@property
@override
def instance_name(self) -> str | None: ...
@instance_name.setter
def instance_name(self, new_name: str | None) -> None: ...
# MARK: Minimal Mixin for Named Classes
[docs]
class NamedMixinMinimal(metaclass=ABCMeta):
def __init_subclass__(cls) -> None:
super().__init_subclass__()
assert hasattr(cls, "instance_name") or type_hints.get_type_hint(cls, "instance_name"), (
f"{cls.__name__} must have an 'instance_name' property to use NamedMixinMinimal"
)
@property
def final_instance_name(self) -> str:
if (name := self.instance_name) is None: # pyright: ignore[reportAttributeAccessIssue] as this mixin must only be used when instance_name is accessible
name = self.get_default_name()
return name
@property
def instance_short_name(self) -> str | None:
"""Get a shortened version of the instance name.
Returns:
str: The shortened instance name.
"""
name = self.instance_name # pyright: ignore[reportAttributeAccessIssue] as this mixin must only be used when instance_name is accessible
return shorten_name(name) if name is not None else None
@property
def final_instance_short_name(self) -> str:
return shorten_name(self.final_instance_name)
[docs]
@classmethod
def get_default_name(cls) -> str:
"""Get the default name for logging.
Returns:
str: The default name.
"""
name = getattr(cls, "__name__", None)
if name is None:
msg = "Could not determine default name"
raise ValueError(msg)
return name
@property
def __repr_name(self) -> str:
"""Get the name to use in __repr__ output.
Returns:
str: The name for __repr__.
"""
nm = self.final_instance_name
cnm = type(self).__name__
if cnm in nm:
return nm
else:
return f"{cnm} {nm}"
@override
def __repr__(self) -> str:
"""Get the string representation of the instance.
Returns:
str: The string representation.
"""
return f"<{self.__repr_name}>"
@property
def __str_name(self) -> str:
"""Get the string name for the instance.
Returns:
str: The string name.
"""
nm = self.final_instance_name
cnm = type(self).__name__
if cnm in nm:
return nm
else:
return f"{shorten_name(type(self).__name__)} {nm}"
@override
def __str__(self) -> str:
"""Get the string representation of the instance.
Returns:
str: The string representation.
"""
return f"<{self.__str_name}>"
# MARK: Mixin for Named Classes
[docs]
class NamedMixin(NamedMixinMinimal):
"""Mixin that adds a name to a class instance.
Provides instance_name and related properties for identification, logging, and display purposes.
Used for configuration, logging, and user-facing objects in pygaindalf.
"""
""" Attribute name used to store the instance name
This is used to allow base classes to customise this without needing to override the instance_name property """
NAMED_MIXIN_ATTRIBUTE: ClassVar[str] = "__name"
[docs]
def __init__(self, *args, instance_name: str | None = None, **kwargs) -> None:
"""Initialize the mixin and set the instance name.
Args:
instance_name (Optional[str]): Optional name for the instance.
*args: Additional positional arguments for superclasses.
**kwargs: Additional keyword arguments for superclasses.
"""
super().__init__(*args, **kwargs)
self.instance_name = instance_name
@property
def instance_name(self) -> str | None:
"""Get the instance name, or class name if not set.
Returns:
str: The instance name.
"""
return getattr(self, type(self).NAMED_MIXIN_ATTRIBUTE, None)
@instance_name.setter
def instance_name(self, new_name: str | None) -> None:
"""Set the instance name.
Args:
new_name (str): The new name to set.
"""
setattr(self, type(self).NAMED_MIXIN_ATTRIBUTE, new_name)
from .loggable import LoggableMixin
if isinstance(self, LoggableMixin):
self._reset_log_cache()
def __set_name__(self, owner: type, name: str) -> None:
"""Set the instance name based on the attribute name and owner."""
if self.instance_name is None:
from . import HierarchicalProtocol, NamedProtocol
if isinstance(self, HierarchicalProtocol):
if isinstance(owner, NamedProtocol):
owner_name = owner.instance_name
elif hasattr(owner, "__name__"):
owner_name = owner.__name__
else:
owner_name = type(owner).__name__
self.instance_name = f"{owner_name}.{name}"
else:
self.instance_name = name