# Copyright (c) 2023 Adolfo Gómez
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""
Parted library exceptions control module.
This modules contains the mechanism to control exceptions raised by parted
library, and to threat them on a pythonic way.
:author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import enum
import contextlib
import typing
import logging
from parted import _parted # type: ignore
if typing.TYPE_CHECKING:
import cffi
logger = logging.getLogger(__name__)
# Only one exception handler can be activated at a time
[docs]class PedException:
"""This class represents a parted library exception
Also, provides the mechanism to control exceptions raised by parted library using a callback.
Args:
exception (cffi.FFI.CData): PedException to wrap
"""
ExceptionHandler = typing.Callable[['PedException'], 'PedException.Option']
[docs] class Option(enum.IntEnum):
"""PedException.Option enum
Possible options available to handle exceptions raised by parted library.
They are used as parameter (as a list of possible return values) for the exception handler callback,
but also as return value for the exception handler callback.
We can always, even if not passed as parameter, return PedException.Option.UNHANDLED, to notify parted
library that we didn't handle the exception.
"""
UNHANDLED = 0
FIX = 1
YES = 2
NO = 4
OK = 8
RETRY = 16
IGNORE = 32
CANCEL = 64
def __str__(self):
return 'PedException.Option.' + self.name
def __repr__(self) -> str:
return self.__str__()
[docs] class Type(enum.IntEnum):
"""PedException.Type enum
Possible types of exceptions raised by parted library
"""
INFORMATION = 1
WARNING = 2
ERROR = 3
FATAL = 4
BUG = 5
FEATURE = 6
def __str__(self):
return 'PedException.Type.' + self.name
def __repr__(self) -> str:
return self.__str__()
_exception_handler: typing.ClassVar[typing.Optional[typing.Callable[['PedException'], Option]]] = None
_last_message: typing.ClassVar[str] = ''
_exception: typing.Any = None
def __init__(self, exception: typing.Optional['cffi.FFI.CData'] = None) -> None:
"""Creates a new PedException instance
Args:
exception (cffi.FFI.CData): PedException to wrap. Can be None.
"""
self._exception = exception if exception else _parted.ffi.NULL
def __bool__(self) -> bool:
return bool(self._exception)
@property
def obj(self) -> 'cffi.FFI.CData':
"""wrapped ``PedException*`` object"""
return self._exception
@property
def message(self) -> str:
"""Exception message"""
if not self._exception:
return ''
return _parted.ffi.string(self._exception.message).decode()
@property
def type(self) -> Type:
"""Exception type"""
if not self._exception:
return PedException.Type.INFORMATION
return self.Type(self._exception.type)
@property
def options(self) -> typing.Set[Option]:
"""Exception options"""
if not self._exception:
return set()
return {x for x in self.Option if self._exception.options & x}
[docs] @staticmethod
def last_message() -> str:
"""Returns the last exception message
Returns:
str: Last exception message
"""
return PedException._last_message
[docs] @staticmethod
def register_handler(handler: typing.Callable[['PedException'], 'PedException.Option']) -> None:
"""Register an exception handler
Registers an exception handler, that will be called when a parted exception is raised.
Note that exception handlers MUST return always a value in PedException.Option, or Option.UNHANDLED
Args:
handler (typing.Callable[['PedException'], 'PedException.Option']): _description_
"""
PedException._exception_handler = handler
[docs] @staticmethod
def restore_handler() -> None:
"""Restores the default exception handler"""
PedException._exception_handler = None
[docs] @staticmethod
@contextlib.contextmanager
def with_handler(handler: typing.Callable[['PedException'], 'PedException.Option']) -> typing.Iterator[None]:
"""Context manager to set a handler for exceptions, and restore it when exit.
Args:
handler (typing.Callable[[PedException], PedException.Option]): Exception handler to set
Yields:
None
Note:
This is a context manager, so it can be used with the ``with`` statement
"""
old_handler = PedException._exception_handler
PedException.register_handler(handler)
try:
yield
finally:
PedException.restore_handler()
if old_handler is not None:
PedException.register_handler(old_handler)
[docs] @staticmethod
def throw(type: 'PedException.Type', option: 'PedException.Option', message: str) -> None:
"""Throws a ``PedException``
Args:
type (PedException.Type): Exception type
option (PedException.Option): Exception option
message (str): Exception message
Note:
This is a wrapper around the ``ped_exception_throw`` function. See the parted documentation for more information.
"""
_parted.lib.ped_exception_throw(type.value, option.value, message.encode())
def __str__(self) -> str:
return 'PedException: {} ({})'.format(self.message, self.type)
[docs]@_parted.ffi.def_extern()
def exception_handler(exc: 'cffi.FFI.CData') -> int:
"""Overriden default exception handler
Warning:
This function is called by parted library, and should not be called directly.
Args:
exc (cffi.FFI.CData): The exception raised by parted library (PedException)
Returns:
int: The option to use to handle the exception
"""
try: # pragma: no cover
pedex = PedException(exc)
# Save the last message thrown
PedException._last_message = pedex.message
if PedException._exception_handler is None:
if pedex.type == PedException.Type.ERROR:
logger.error('Error: %s', pedex.message)
elif pedex.type == PedException.Type.WARNING:
logger.warning('Warning: %s', pedex.message)
elif pedex.type == PedException.Type.INFORMATION:
logger.info('Information: %s', pedex.message)
elif pedex.type == PedException.Type.FATAL:
logger.critical('Fatal: %s', pedex.message)
elif pedex.type == PedException.Type.BUG:
logger.critical('Bug: %s', pedex.message)
elif pedex.type == PedException.Type.FEATURE:
logger.critical('No feature: %s', pedex.message)
else:
logger.critical('Unknown exception: %s', pedex.message)
return PedException.Option.UNHANDLED.value
value = PedException._exception_handler(pedex)
# Value must be in received options
if value in pedex.options or value == PedException.Option.UNHANDLED:
return value.value
logger.warning('Invalid exception handler return value: %s (must be one of %s)', value, pedex.options)
except Exception as e: # pragma: no cover
logger.exception('Exception in PedException handler')
return PedException.Option.UNHANDLED.value
# Install OUR default exception handler
_parted.lib.ped_exception_set_handler(_parted.lib.exception_handler)