mirror of
https://github.com/SickGear/SickGear.git
synced 2024-12-18 16:53:38 +00:00
319 lines
9.5 KiB
Python
319 lines
9.5 KiB
Python
|
"""
|
||
|
Parent of all field classes in :mod:`hachoir.field`.
|
||
|
"""
|
||
|
|
||
|
from hachoir.stream import InputFieldStream
|
||
|
from hachoir.core.log import Logger
|
||
|
from hachoir.core.tools import makePrintable
|
||
|
from weakref import ref as weakref_ref
|
||
|
|
||
|
|
||
|
class FieldError(Exception):
|
||
|
"""
|
||
|
Error raised by a :class:`Field`
|
||
|
"""
|
||
|
pass
|
||
|
|
||
|
|
||
|
def joinPath(path, name):
|
||
|
if path != "/":
|
||
|
return "/".join((path, name))
|
||
|
else:
|
||
|
return "/%s" % name
|
||
|
|
||
|
|
||
|
class MissingField(KeyError, FieldError):
|
||
|
|
||
|
def __init__(self, field, key):
|
||
|
KeyError.__init__(self)
|
||
|
self.field = field
|
||
|
self.key = key
|
||
|
|
||
|
def __str__(self):
|
||
|
return 'Can\'t get field "%s" from %s' % (self.key, self.field.path)
|
||
|
|
||
|
|
||
|
class Field(Logger):
|
||
|
static_size = None
|
||
|
"""(optional) Helper to compute field size.
|
||
|
|
||
|
May have types:
|
||
|
None: field size is computed dynamically.
|
||
|
int: field size, in bits.
|
||
|
callable: function that receives the same arguments as the constructor,
|
||
|
without ``parent``.
|
||
|
"""
|
||
|
|
||
|
is_field_set = False
|
||
|
"""bool: True if this field contains other fields (ie. is a field set),
|
||
|
False otherwise.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, parent, name, size=None, description=None):
|
||
|
"""Set default class attributes, set right address if None address is
|
||
|
given.
|
||
|
|
||
|
Args:
|
||
|
parent (Field): Parent field of this field
|
||
|
name (str): The name of the field, it must be unique in `parent`. If
|
||
|
it ends with `[]`, end will be replaced with "[new_id]" (eg.
|
||
|
"raw[]" becomes "raw[0]", next will be "raw[1]", and then
|
||
|
"raw[2]", etc.)
|
||
|
size (int, optional): Size of the field in bits. If `None` then it
|
||
|
will be computed later.
|
||
|
address (int, optional): Address in bit relative to the parent
|
||
|
absolute address.
|
||
|
description (str, optional): String description
|
||
|
"""
|
||
|
assert issubclass(parent.__class__, Field)
|
||
|
assert (size is None) or (0 <= size)
|
||
|
self._parent = parent
|
||
|
if not name:
|
||
|
raise ValueError("empty field name")
|
||
|
self._name = name
|
||
|
self._address = parent.nextFieldAddress()
|
||
|
self._size = size
|
||
|
self._description = description
|
||
|
|
||
|
def _logger(self):
|
||
|
return self.path
|
||
|
|
||
|
def createDescription(self):
|
||
|
"""Override in derived classes to provide :attr:`description`."""
|
||
|
return ""
|
||
|
|
||
|
@property
|
||
|
def description(self):
|
||
|
"""str: Informal description of this field. Cached.
|
||
|
|
||
|
The description of a field may provide a general summary of its usage
|
||
|
or for field sets it can be used to give a short indication of the
|
||
|
contents without having to expand the node.
|
||
|
"""
|
||
|
if self._description is None:
|
||
|
try:
|
||
|
self._description = self.createDescription()
|
||
|
if isinstance(self._description, str):
|
||
|
self._description = makePrintable(
|
||
|
self._description, "ISO-8859-1")
|
||
|
except Exception as err:
|
||
|
self.error("Error getting description: " + str(err))
|
||
|
self._description = ""
|
||
|
return self._description
|
||
|
|
||
|
def __str__(self):
|
||
|
"""Alias for :attr:`display`."""
|
||
|
return self.display
|
||
|
|
||
|
def __repr__(self):
|
||
|
return "<%s path=%r, address=%s, size=%s>" % (
|
||
|
self.__class__.__name__, self.path, self._address, self._size)
|
||
|
|
||
|
def hasValue(self):
|
||
|
"""bool: Check if field has a value."""
|
||
|
return self.value is not None
|
||
|
|
||
|
def createValue(self):
|
||
|
"""Override in derived classes to provide :attr:`value`."""
|
||
|
raise NotImplementedError()
|
||
|
|
||
|
@property
|
||
|
def value(self):
|
||
|
"""Value of field."""
|
||
|
try:
|
||
|
return self.__value
|
||
|
except AttributeError:
|
||
|
try:
|
||
|
self.__value = self.createValue()
|
||
|
except Exception as err:
|
||
|
self.error("Unable to create value: %s" % str(err))
|
||
|
self.__value = None
|
||
|
return self.__value
|
||
|
|
||
|
@property
|
||
|
def parent(self):
|
||
|
"""GenericFieldSet: Parent of this field."""
|
||
|
return self._parent
|
||
|
|
||
|
def createDisplay(self):
|
||
|
"""Override in derived classes to provide :attr:`display`."""
|
||
|
return str(self.value)
|
||
|
|
||
|
@property
|
||
|
def display(self):
|
||
|
"""str: Short, human-friendly string representing field contents."""
|
||
|
try:
|
||
|
return self.__display
|
||
|
except AttributeError:
|
||
|
try:
|
||
|
self.__display = self.createDisplay()
|
||
|
except Exception as err:
|
||
|
self.error("Unable to create display: %s" % err)
|
||
|
self.__display = ""
|
||
|
return self.__display
|
||
|
|
||
|
def createRawDisplay(self):
|
||
|
value = self.value
|
||
|
if isinstance(value, str):
|
||
|
return makePrintable(value, "ASCII")
|
||
|
else:
|
||
|
return str(value)
|
||
|
|
||
|
@property
|
||
|
def raw_display(self):
|
||
|
"""str: Represents raw field content"""
|
||
|
try:
|
||
|
return self.__raw_display
|
||
|
except AttributeError:
|
||
|
try:
|
||
|
self.__raw_display = self.createRawDisplay()
|
||
|
except Exception as err:
|
||
|
self.error("Unable to create raw display: %s" % err)
|
||
|
self.__raw_display = ""
|
||
|
return self.__raw_display
|
||
|
|
||
|
@property
|
||
|
def name(self):
|
||
|
"""str: Field name, unique in its parent field set list."""
|
||
|
return self._name
|
||
|
|
||
|
@property
|
||
|
def index(self):
|
||
|
"""int: index of the field in parent field set, starting from 0."""
|
||
|
if not self._parent:
|
||
|
return None
|
||
|
return self._parent.getFieldIndex(self)
|
||
|
|
||
|
@property
|
||
|
def path(self):
|
||
|
"""str: Full path of this field starting from the root field."""
|
||
|
if not self._parent:
|
||
|
return '/'
|
||
|
names = []
|
||
|
field = self
|
||
|
while field is not None:
|
||
|
names.append(field._name)
|
||
|
field = field._parent
|
||
|
names[-1] = ''
|
||
|
return '/'.join(reversed(names))
|
||
|
|
||
|
@property
|
||
|
def address(self):
|
||
|
"""int: Relative address to parent address, in bits."""
|
||
|
return self._address
|
||
|
|
||
|
@property
|
||
|
def absolute_address(self):
|
||
|
"""int: Absolute address (from beginning of stream), in bits."""
|
||
|
address = self._address
|
||
|
current = self._parent
|
||
|
while current:
|
||
|
address += current._address
|
||
|
current = current._parent
|
||
|
return address
|
||
|
|
||
|
@property
|
||
|
def size(self):
|
||
|
"""int: Size of this field, in bits. Cached."""
|
||
|
return self._size
|
||
|
|
||
|
def _getField(self, name, const):
|
||
|
if name.strip("."):
|
||
|
return None
|
||
|
field = self
|
||
|
for index in range(1, len(name)):
|
||
|
field = field._parent
|
||
|
if field is None:
|
||
|
break
|
||
|
return field
|
||
|
|
||
|
def getField(self, key, const=True):
|
||
|
"""
|
||
|
Args:
|
||
|
key (str): relative or absolute path for the desired field.
|
||
|
const (bool): For field sets, whether to consume additional input to
|
||
|
find a matching field.
|
||
|
|
||
|
Returns:
|
||
|
Field: The field matching the provided path.
|
||
|
"""
|
||
|
if key:
|
||
|
if key[0] == "/":
|
||
|
if self._parent:
|
||
|
current = self._parent.root
|
||
|
else:
|
||
|
current = self
|
||
|
if len(key) == 1:
|
||
|
return current
|
||
|
key = key[1:]
|
||
|
else:
|
||
|
current = self
|
||
|
for part in key.split("/"):
|
||
|
field = current._getField(part, const)
|
||
|
if field is None:
|
||
|
raise MissingField(current, part)
|
||
|
current = field
|
||
|
return current
|
||
|
raise KeyError("Key must not be an empty string!")
|
||
|
|
||
|
def __getitem__(self, key):
|
||
|
"""Alias for :meth:`getField`, with ``const=False``"""
|
||
|
return self.getField(key, False)
|
||
|
|
||
|
def __contains__(self, key):
|
||
|
"""Check whether a field set contains the provided field.
|
||
|
|
||
|
Args:
|
||
|
key (str): The path to the field.
|
||
|
|
||
|
Returns:
|
||
|
bool
|
||
|
"""
|
||
|
try:
|
||
|
return self.getField(key, False) is not None
|
||
|
except FieldError:
|
||
|
return False
|
||
|
|
||
|
def _createInputStream(self, **args):
|
||
|
"""Override in derived classes to provide the input stream returned by
|
||
|
:meth:`getSubIStream`.
|
||
|
|
||
|
Returns:
|
||
|
InputFieldStream
|
||
|
"""
|
||
|
assert self._parent
|
||
|
return InputFieldStream(self, **args)
|
||
|
|
||
|
def getSubIStream(self):
|
||
|
"""
|
||
|
Returns:
|
||
|
InputFieldStream: an input stream containing the field content.
|
||
|
"""
|
||
|
if hasattr(self, "_sub_istream"):
|
||
|
stream = self._sub_istream()
|
||
|
else:
|
||
|
stream = None
|
||
|
if stream is None:
|
||
|
stream = self._createInputStream()
|
||
|
self._sub_istream = weakref_ref(stream)
|
||
|
return stream
|
||
|
|
||
|
def setSubIStream(self, createInputStream):
|
||
|
"""
|
||
|
Args:
|
||
|
createInputStream (``callback(cis, **args)``): Function to use in place of
|
||
|
:meth:`_createInputStream`. Receives the previous value of
|
||
|
``_createInputStream`` as its first argument, for chaining.
|
||
|
"""
|
||
|
cis = self._createInputStream
|
||
|
self._createInputStream = lambda **args: createInputStream(cis, **args)
|
||
|
|
||
|
def __bool__(self):
|
||
|
"""Method called by code like "if field: (...)".
|
||
|
Always returns True
|
||
|
"""
|
||
|
return True
|
||
|
|
||
|
def getFieldType(self):
|
||
|
return self.__class__.__name__
|