Source code for qbraid.runtime.options

# Copyright (C) 2024 qBraid
#
# This file is part of the qBraid-SDK
#
# The qBraid-SDK is free software released under the GNU General Public License v3
# or later. You can redistribute and/or modify it under the terms of the GPL v3.
# See the LICENSE file in the project root or <https://www.gnu.org/licenses/gpl-3.0.html>.
#
# THERE IS NO WARRANTY for the qBraid-SDK, as per Section 15 of the GPL v3.

"""
Module for managing qBraid-specific runtime options.

"""

import copy
from dataclasses import dataclass
from dataclasses import field as dataclass_field
from typing import Any, Callable


[docs] @dataclass class RuntimeOptions: """ Manages qBraid-specific options with controlled defaults and dynamic fields. The `RuntimeOptions` class allows you to initialize options with default fields, add dynamic fields, and validate the values of specific fields using custom validators. It also provides dictionary-like access to option fields. Example: .. code-block:: python >>> from qbraid.runtime.options import RuntimeOptions # Initialize options with default and custom fields >>> options = RuntimeOptions(check=True, custom_field=42) # Access option fields like dictionary >>> options["check"] True # Add dynamic fields >>> options["new_field"] = "hello" >>> print(options) RuntimeOptions(check=True, custom_field=42, new_field='hello') # Set a validator for a field >>> options.set_validator("check", lambda x: isinstance(x, bool)) # Attempting to set an invalid value will raise a ValueError >>> options.check = False # Valid >>> options.check = "invalid_value" # Raises ValueError # Make a shallow copy of the options >>> options_copy = copy.copy(options) >>> print(options_copy) RuntimeOptions(check=False, custom_field=42, new_field='hello') Args: kwargs: Keyword arguments representing the initial options to set. These will form the default fields of the `Options` instance. Attributes: _fields (dict): The internal dictionary storing option fields. _validators (dict): A dictionary storing field validators. """ _validators: dict[str, Callable[[Any], bool]] = dataclass_field( default_factory=dict, repr=False )
[docs] def __init__(self, **kwargs: Any): reserved_keywords = {"set_validator", "validate_option", "update_options", "get"} for key in kwargs: if key in reserved_keywords: raise ValueError(f"The option name '{key}' is reserved and cannot be used.") if key.startswith("__"): raise ValueError(f"The option name '{key}' with dunder prefix is not allowed.") object.__setattr__(self, "_default_fields", kwargs.copy()) object.__setattr__(self, "_fields", kwargs.copy()) object.__setattr__(self, "_validators", {})
def set_validator(self, option_name: str, validator: Callable[[Any], bool]): """Sets a validator for a specific field.""" if not hasattr(self, option_name) and option_name not in self._fields: raise KeyError(f"Field '{option_name}' is not present in options.") self._validators[option_name] = validator def validate_option(self, option_name: str, value: Any): """Validates a field's value using the registered validator, if any. Raises: ValueError: If the validator function raises an exception or returns False. """ validator = self._validators.get(option_name) if validator: try: is_valid = validator(value) except Exception as err: raise ValueError( f"Validator for field '{option_name}' raised an exception: {err}" ) from err if not is_valid: raise ValueError(f"Value '{value}' is not valid for field '{option_name}'.") def update_options(self, **new_options): """Updates multiple options with validation.""" for option_name, value in new_options.items(): if hasattr(self, option_name): self.validate_option(option_name, value) setattr(self, option_name, value) else: self._fields[option_name] = value def get(self, key: str, default: Any = None) -> Any: """Return the value for the given key, or the default value if not found.""" if hasattr(self, key): return getattr(self, key) return self._fields.get(key, default) def __getitem__(self, key: str) -> Any: """Allow dictionary-like access.""" if hasattr(self, key): return getattr(self, key) return self._fields[key] def __setitem__(self, key: str, value: Any): """Allow dictionary-like updates.""" self.update_options(**{key: value}) def __delitem__(self, key: str): """Prevent deletion of default fields but allow deletion of additional fields.""" if key in self._default_fields: raise KeyError(f"Cannot delete default field '{key}'.") if key in self._fields: del self._fields[key] else: raise KeyError(f"Field '{key}' not found in options.") def __getattr__(self, name: str) -> Any: """Access options like attributes.""" if name in {"_fields", "_default_fields", "_validators"}: return self.__dict__.get(name) if name in self._fields: return self._fields[name] raise AttributeError(f"Option '{name}' is not defined.") def __setattr__(self, name: str, value: Any): """Set options like attributes, ensuring validation if necessary.""" if name in {"_fields", "_default_fields", "_validators"}: super().__setattr__(name, value) elif name in self._fields or name in self._default_fields: if self._fields.get(name) != value: if name in self._validators: self.validate_option(name, value) self._fields[name] = value else: self.update_options(**{name: value}) @property def __dict__(self): """Returns the internal fields as a dictionary.""" return self._fields def __iter__(self): """Allow iteration over the key-value pairs of the internal fields.""" return iter(self._fields.items()) def __len__(self): """Return the number of fields.""" return len(self._fields) def __repr__(self): """Returns a string representation of the options.""" options_str = ", ".join(f"{k}={v!r}" for k, v in self._fields.items()) return f"RuntimeOptions({options_str})" def __copy__(self): """Create a shallow copy of the RuntimeOptions object.""" cls = self.__class__ new_instance = cls(**self._fields) new_instance._validators = copy.copy(self._validators) return new_instance def __eq__(self, other: Any) -> bool: """Custom equality check for RuntimeOptions objects.""" if not isinstance(other, RuntimeOptions): return False if self._fields != other._fields: return False if set(self._validators.keys()) != set(other._validators.keys()): return False return True def merge(self, other: "RuntimeOptions", override_validators: bool = True): """Merges another RuntimeOptions instance into this one. Args: other (RuntimeOptions): The RuntimeOptions instance to merge from. override_validators (bool): Determines whether validators from `other` should override existing validators in `self`. Raises: ValueError: If any option value is invalid after merging. """ combined_validators = self._validators.copy() if override_validators: combined_validators.update(other._validators) else: for key, validator in other._validators.items(): combined_validators.setdefault(key, validator) combined_fields = self._fields.copy() combined_fields.update(other.__dict__) object.__setattr__(self, "_validators", combined_validators) object.__setattr__(self, "_fields", combined_fields) if override_validators: options_to_validate = combined_fields.items() else: options_to_validate = ((key, combined_fields[key]) for key in other.__dict__.keys()) for option_name, value in options_to_validate: validator = combined_validators.get(option_name) if validator and not validator(value): raise ValueError( f"Value '{value}' is not valid for field '{option_name}' after merging." )