Source code for domprob.announcements.validation.chain_validation

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, TypeVar

from domprob.announcements.exceptions import AnnouncementException
from domprob.announcements.validation.base_validator import BaseValidator

if TYPE_CHECKING:
    from domprob.announcements.validation.chain import (  # pragma: no cover
        ValidationChain,
    )

# Typing helper: defines a validator implementing the abstract class
_ChainLink = TypeVar("_ChainLink", bound=BaseValidator)


[docs] class ValidationChainException(AnnouncementException): """Base exception class for errors related to validation chains. This exception serves as the root for all validation chain-related errors, such as issues with chain construction, execution, or invalid links. It is designed to be extended by more specific exceptions within the validation framework. """
[docs] class InvalidLinkException(ValidationChainException): """Exception raised when an invalid link is added to a validation chain. This exception is used to indicate that a link in the validation chain does not meet the required criteria or fails validation. It provides detailed information about the invalid link and the expected type. Attributes: link (Any): The invalid link that caused the exception. expected_type (type[Any]): The expected type for valid links in the chain. Args: link (Any): The link object that is invalid. expected_type (type[Any]): The expected type for links in the validation chain. Examples: >>> from domprob.exceptions import InvalidLinkException >>> >>> def raise_invalid_link(): ... raise InvalidLinkException("InvalidLink", expected_type=int) ... >>> try: ... raise_invalid_link() ... except InvalidLinkException as e: ... print(str(e)) Invalid link of type 'str', expected type 'int' """ def __init__(self, link: Any, expected_type: type[Any]) -> None: self.link = link self.expected_type = expected_type super().__init__(self.msg) @property def msg(self) -> str: """Constructs a detailed error message for the exception. This property dynamically generates a human-readable string that describes the invalid link and the expected type. Returns: str: A string describing the invalid link and its expected type. Examples: >>> exc = InvalidLinkException("InvalidLink", expected_type=int) >>> exc.msg "Invalid link of type 'str', expected type 'int'" """ l_name = type(self.link).__name__ e_name = self.expected_type.__name__ return f"Invalid link of type '{l_name}', expected type '{e_name}'"
[docs] class EmptyChainException(ValidationChainException): """Exception raised when a validation chain is empty. This exception indicates that no validators have been added to a validation chain, which prevents the chain from performing any meaningful validation. Args: chain (ValidationChain): The empty validation chain. Attributes: chain (ValidationChain): The empty validation chain. Examples: >>> from domprob.exceptions import EmptyChainException >>> from domprob.announcements.validation.base_validator import BaseValidator >>> from domprob.announcements.validation.chain import ValidationChain >>> >>> try: ... raise EmptyChainException(ValidationChain(BaseValidator)) ... except EmptyChainException as e: ... print(e) ... Nothing to validate, no links added to chain 'ValidationChain(base='BaseValidator')' """ def __init__(self, chain: ValidationChain) -> None: self.chain = chain super().__init__(self.msg) @property def msg(self) -> str: """Returns the error message associated with the exception. Returns: str: The error message describing the empty validation chain issue. Examples: >>> from domprob.exceptions import EmptyChainException >>> from domprob.announcements.validation.base_validator import BaseValidator >>> from domprob.announcements.validation.chain import ValidationChain >>> >>> exc = EmptyChainException(ValidationChain(BaseValidator)) >>> exc.msg "Nothing to validate, no links added to chain 'ValidationChain(base='BaseValidator')'" """ return f"Nothing to validate, no links added to chain '{self.chain!r}'"
[docs] class LinkExistsException(ValidationChainException): # pylint: disable=line-too-long """Exception raised when a duplicate link is added to a validation chain. This exception is used to indicate that the specified link already exists in the validation chain and duplicates are not allowed. It provides details about the duplicate link and the chain where the conflict occurred. Args: link (Any): The duplicate link that caused the exception. chain (ValidationChain): The validation chain where the duplicate was found. Attributes: link (Any): The duplicate link. chain (ValidationChain): The chain where the duplicate was detected. Examples: >>> from domprob.exceptions import LinkExistsException >>> from domprob.announcements.validation.base_validator import BaseValidator >>> from domprob.announcements.validation.chain import ValidationChain >>> >>> try: ... raise LinkExistsException("DuplicateLink", chain=ValidationChain(BaseValidator)) # type: ignore ... except LinkExistsException as e: ... print(e) ... Link ''DuplicateLink'' already exists in chain 'ValidationChain(base='BaseValidator')' """ def __init__(self, link: _ChainLink, chain: ValidationChain) -> None: self.link = link self.chain = chain super().__init__(self.msg) @property def msg(self) -> str: """Constructs a detailed error message indicating a duplicate link in the validation chain. Returns: str: A message indicating the duplicate link and the affected chain. Examples: >>> from domprob.exceptions import LinkExistsException >>> from domprob.announcements.validation.base_validator import BaseValidator >>> from domprob.announcements.validation.chain import ValidationChain >>> >>> chain = ValidationChain(base=BaseValidator) >>> exc = LinkExistsException("DuplicateLink", chain) # type: ignore >>> exc.msg "Link ''DuplicateLink'' already exists in chain 'ValidationChain(base='BaseValidator')'" """ return f"Link '{self.link!r}' already exists in chain '{self.chain!r}'"
# pylint: disable=too-few-public-methods
[docs] class ABCLinkValidator(ABC): """Abstract base class for validators in a validation chain. This class defines the structure for implementing validators that perform specific checks on links within a validation chain. Attributes: chain (ValidationChain): The validation chain associated with this validator. It provides context for the validation process. Args: chain (ValidationChain): The validation chain to associate with this validator. Examples: >>> from domprob.announcements.validation.chain import ABCLinkValidator >>> from domprob.announcements.validation.base_validator import BaseValidator >>> >>> class ExampleValidator(ABCLinkValidator): ... def validate(self, link: BaseValidator) -> None: ... pass ... """ def __init__(self, chain: ValidationChain) -> None: self.chain = chain
[docs] @abstractmethod def validate(self, link: _ChainLink) -> None: """Validates a single link in the validation chain. This abstract method must be implemented by subclasses to define specific validation logic. Args: link (_ChainLink): The link to validate. Raises: NotImplementedError: If the subclass does not implement this method. """ raise NotImplementedError # pragma: no cover
[docs] def __repr__(self) -> str: """Returns a string representation of the validator. Returns: str: A string representation of the validator, including its class name and the validation chain it belongs to. Examples: >>> from domprob.announcements.validation.chain import ABCLinkValidator >>> from domprob.announcements.validation.base_validator import BaseValidator >>> >>> class ExampleValidator(ABCLinkValidator): ... def validate(self, link: BaseValidator) -> None: ... pass ... >>> from domprob.announcements.validation.chain import ValidationChain >>> >>> validator = ExampleValidator(ValidationChain(BaseValidator)) >>> repr(validator) "ExampleValidator(ValidationChain(base='BaseValidator'))" """ return f"{self.__class__.__name__}({self.chain!r})"
[docs] class LinkTypeValidator(ABCLinkValidator): """Validator to ensure that links in the validation chain are of the expected type. This validator checks whether each link added to the validation chain is an instance of the chain's base type. If a link does not match the expected type, it raises an `InvalidLinkException`. This ensures type safety and consistency within the chain. Attributes: chain (ValidationChain): The validation chain this validator is associated with. Examples: >>> from domprob.announcements.validation.chain_validation import LinkTypeValidator >>> from domprob.announcements.validation.chain import ValidationChain >>> from domprob.announcements.validation.base_validator import BaseValidator >>> >>> validator = LinkTypeValidator(ValidationChain(BaseValidator)) >>> >>> try: ... validator.validate(123) # type: ignore ... except InvalidLinkException as e: ... print(e) ... Invalid link of type 'int', expected type 'BaseValidator' """
[docs] def validate(self, link: _ChainLink) -> None: """Validates that the provided link is of the expected type. This method ensures that the given link is an instance of the chain's base type. If the link is not of the expected type, it raises an `InvalidLinkException`. Args: link (_ChainLink): The link to validate. Raises: InvalidLinkException: If the link is not of the expected type. Examples: >>> from domprob.announcements.validation.chain_validation import LinkTypeValidator >>> from domprob.announcements.validation.chain import ValidationChain >>> from domprob.announcements.validation.base_validator import BaseValidator >>> >>> validator = LinkTypeValidator(ValidationChain(BaseValidator)) >>> >>> try: ... validator.validate(123) # type: ignore ... except InvalidLinkException as e: ... print(e) ... Invalid link of type 'int', expected type 'BaseValidator' """ if not isinstance(link, self.chain.base): raise InvalidLinkException(link, self.chain.base)
[docs] class UniqueLinkValidator(ABCLinkValidator): # pylint: disable=line-too-long """Validator to ensure that links in the validation chain are unique. This validator checks whether a link already exists in the validation chain. If a duplicate link is detected, it raises a `LinkExistsException`. This ensures that all links in the chain are unique. Attributes: chain (ValidationChain): The validation chain this validator is associated with. Examples: >>> from domprob.announcements.validation.chain_validation import LinkTypeValidator >>> from domprob.announcements.validation.chain import ValidationChain >>> from domprob.announcements.validation.base_validator import BaseValidator >>> >>> chain = ValidationChain(BaseValidator) >>> validator = UniqueLinkValidator(chain) >>> >>> class ExampleValidator(BaseValidator): ... def validate(self, link: BaseValidator) -> None: ... pass ... >>> example_validator = ExampleValidator() >>> chain.append(example_validator) >>> >>> try: ... validator.validate(example_validator) ... except LinkExistsException as e: ... print(e) ... Link 'ExampleValidator(next_=None)' already exists in chain 'ValidationChain(base='BaseValidator')' """
[docs] def validate(self, link: _ChainLink) -> None: # noinspection PyShadowingNames """Validates that the provided link does not already exist in the validation chain. This method checks whether the given link is already present in the validation chain. If the link is a duplicate, it raises a `LinkExistsException`. This ensures that all links within the chain are unique. Args: link (_ChainLink): The link to validate. Raises: LinkExistsException: If the link already exists in the validation chain. Examples: >>> from domprob.announcements.validation.chain_validation import LinkTypeValidator >>> from domprob.announcements.validation.chain import ValidationChain >>> from domprob.announcements.validation.base_validator import BaseValidator >>> >>> chain = ValidationChain(BaseValidator) >>> validator = UniqueLinkValidator(chain) >>> >>> class ExampleValidator(BaseValidator): ... def validate(self, link: BaseValidator) -> None: ... pass ... >>> example_validator = ExampleValidator() >>> chain.append(example_validator) >>> >>> try: ... validator.validate(example_validator) ... except LinkExistsException as e: ... print(e) ... Link 'ExampleValidator(next_=None)' already exists in chain 'ValidationChain(base='BaseValidator')' """ if link in self.chain: raise LinkExistsException(link, self.chain)
[docs] class ABCLinkValidatorContext(ABC): """Abstract base class for context-aware link validators in a validation chain. This class provides an interface for validators that need additional context about the validation chain during the validation process. It enforces the implementation of the `validate` method, allowing subclasses to perform more sophisticated validations that depend on the state of the chain. Attributes: chain (ValidationChain): The validation chain associated with this validator. It provides context for the validation process. Args: chain (ValidationChain): The validation chain to associate with this context-aware validator. Examples: >>> from domprob.announcements.validation.chain import ( ... ABCLinkValidatorContext, ValidationChain ... ) >>> from domprob.announcements.validation.base_validator import BaseValidator >>> >>> class ExampleValidator(BaseValidator): ... def validate(self, link: BaseValidator) -> None: ... pass ... >>> >>> class ExampleContext(ABCLinkValidatorContext): ... def add_validators(self, *validators: type[BaseValidator]) -> None: ... pass ... ... def validate(self, link: BaseValidator) -> None: ... pass ... >>> chain = ValidationChain(BaseValidator) >>> validator_context = ExampleContext(chain) >>> validator_context.validate(ExampleValidator()) """ def __init__( self, chain: ValidationChain, *validators: type[ABCLinkValidator] ) -> None: self.chain = chain self.validators: list[type[ABCLinkValidator]] = [] self.add_validators(*validators)
[docs] @abstractmethod def add_validators(self, *validators: type[ABCLinkValidator]) -> None: # noinspection PyShadowingNames """Adds one or more validators to the validation chain. This abstract method is designed to allow additional validators to be dynamically added to the validation chain during runtime. Each validator is appended to the chain, enabling customisation and extensibility of the validation process. Args: *validators (ABCLinkValidator): One or more validator instances to add to the validation chain. Examples: >>> from domprob.announcements.validation.chain import ( ... ABCLinkValidatorContext, ValidationChain ... ) >>> from domprob.announcements.validation.base_validator import BaseValidator >>> >>> class ExampleValidator(BaseValidator): ... def validate(self, link: BaseValidator) -> None: ... pass ... >>> class ExampleContext(ABCLinkValidatorContext): ... def add_validators(self, *validators: type[BaseValidator]) -> None: ... pass ... ... def validate(self, link: BaseValidator) -> None: ... pass ... >>> chain = ValidationChain(ExampleValidator) >>> context = ExampleContext(chain) >>> context.add_validators(ExampleValidator, ExampleValidator) """ raise NotImplementedError # pragma: no cover
[docs] @abstractmethod def validate(self, link: _ChainLink) -> None: # noinspection PyShadowingNames """Abstract method to validate a link in the chain using additional context. This method must be implemented by subclasses to provide logic for validating a link with additional contextual information. This allows the validator to account for dynamic rules or states during validation. Args: link (_ChainLink): The link to validate. Raises: NotImplementedError: If a subclass does not implement this method. Examples: >>> from domprob.announcements.validation.chain import ( ... ABCLinkValidatorContext, ValidationChain ... ) >>> >>> class ExampleValidator(BaseValidator): ... def validate(self, link: BaseValidator) -> None: ... pass ... >>> >>> class ExampleContext(ABCLinkValidatorContext): ... def add_validators(self, *validators: type[BaseValidator]) -> None: ... pass ... ... def validate(self, link: BaseValidator) -> None: ... pass ... >>> chain = ValidationChain(ExampleValidator) >>> validator_context = ExampleContext(chain) >>> validator_context.validate(ExampleValidator()) """ raise NotImplementedError # pragma: no cover
[docs] def __repr__(self) -> str: """Returns a string representation of the context-aware validator. Returns: str: A string representation of the validator, showing the class name and the validation chain it is associated with. Examples: >>> from domprob.announcements.validation.chain import ( ... ABCLinkValidatorContext, ... BaseValidator, ... ValidationChain ... ) >>> >>> class ExampleValidator(BaseValidator): ... def validate(self, link: BaseValidator) -> None: ... pass ... >>> class ExampleContext(ABCLinkValidatorContext): ... def add_validators(self, *validators: type[BaseValidator]) -> None: ... pass ... ... def validate(self, link: BaseValidator) -> None: ... pass ... >>> chain = ValidationChain(ExampleValidator) >>> context = ExampleContext(chain) >>> repr(context) "ExampleContext(chain=ValidationChain(base='ExampleValidator'))" """ return f"{self.__class__.__name__}(chain={self.chain!r})"
[docs] class LinkValidatorContext(ABCLinkValidatorContext): """Concrete implementation of a context-aware link validator. The `LinkValidatorContext` class extends `ABCLinkValidatorContext` to provide validation logic for links in a chain, utilising additional contextual information. It allows flexible and dynamic validation of links, depending on the state of the chain or external conditions. Attributes: chain (ValidationChain): The validation chain associated with this context-aware validator. Args: chain (ValidationChain): The validation chain to associate with the validator. Examples: >>> from domprob.announcements.validation.chain import ( ... ABCLinkValidatorContext, ValidationChain ... ) >>> from domprob.announcements.validation.chain_validation import InvalidLinkException >>> >>> class ExampleLink: ... pass ... >>> chain = ValidationChain(BaseValidator) >>> validator_context = LinkValidatorContext(chain) >>> try: ... validator_context.validate("invalid_link") # type: ignore ... except InvalidLinkException as e: ... print(e) Invalid link of type 'str', expected type 'BaseValidator' """ DEFAULT_VALIDATORS: tuple[type[ABCLinkValidator], ...] = ( LinkTypeValidator, UniqueLinkValidator, ) def __init__( self, chain: ValidationChain, *validators: type[ABCLinkValidator] ) -> None: super().__init__(chain, *self.DEFAULT_VALIDATORS, *validators)
[docs] def add_validators(self, *validators: type[ABCLinkValidator]) -> None: """Adds one or more validators to the validation chain. This method allows the dynamic addition of validators to the validation chain at runtime. Each provided validator is appended to the chain, enabling custom validation logic and extensibility. Args: *validators (ABCLinkValidator): One or more validator instances to add to the validation chain. Examples: >>> from domprob.announcements.validation.chain import ( ... ABCLinkValidatorContext, ValidationChain ... ) >>> from domprob.announcements.validation.base_validator import BaseValidator >>> >>> class ExampleValidator(ABCLinkValidator): ... def validate(self, link: BaseValidator) -> None: ... pass ... >>> chain = ValidationChain(BaseValidator) >>> context = LinkValidatorContext(chain) >>> context.add_validators(ExampleValidator, ExampleValidator) """ self.validators.extend(validators)
[docs] def validate(self, *links: _ChainLink) -> None: """Validates a link in the chain. This method performs validation on the given links using the validators in the validation chain. It ensures that the link meets all the criteria enforced by the chain's validators. Args: *links (_ChainLink): The links to validate. Raises: Any: Exceptions raised by the individual validators in the chain if the link does not meet the required criteria. Examples: >>> from domprob.announcements.validation.chain import ( ... ABCLinkValidatorContext, ValidationChain ... ) >>> from domprob.announcements.validation.chain_validation import InvalidLinkException >>> >>> class ExampleLink: ... pass ... >>> chain = ValidationChain(BaseValidator) >>> validator_context = LinkValidatorContext(chain) >>> try: ... validator_context.validate("invalid_link") # type: ignore ... except InvalidLinkException as e: ... print(e) Invalid link of type 'str', expected type 'BaseValidator' """ for validator in self.validators: for link in links: validator(self.chain).validate(link)
[docs] def __repr__(self) -> str: """Returns a string representation of the `LinkValidatorContext`. Returns: str: A string representation of the context, including its class name and the associated validation chain. Examples: >>> from domprob.announcements.validation.chain import ( ... ABCLinkValidatorContext, ValidationChain ... ) >>> >>> class ExampleLink: ... pass ... >>> chain = ValidationChain(BaseValidator) >>> validator_context = LinkValidatorContext(chain) >>> repr(validator_context) "LinkValidatorContext(ValidationChain(base='BaseValidator'))" """ return f"{self.__class__.__name__}({self.chain!r})"