def _fallback_obj_container(tag):
def _impl_modules():
def _detect_non_level_objects_other(section):
def _fallback_levelobject(section):
def _blacklist_nonlevel(section):
def write_autoload_modules(): UTF_16_DECODE = codecs.getdecoder('utf-16-le')
UTF_16_ENCODE = codecs.getencoder('utf-16-le')
S_COLOR_RGBA = Struct("<4f")
S_FLOAT = Struct("<f")
S_DOUBLE = Struct("<d")
S_FLOAT3 = Struct("<fff")
S_FLOAT4 = Struct("<ffff")
S_BYTE = Struct('b')
S_INT = Struct('<i')
S_LONG = Struct('<q')
S_UINT = Struct('<I')
S_ULONG = Struct('<Q')
S_UINT2 = Struct("<II")
S_UINT3 = Struct("<III")
SKIP_BYTES = b'\xFD\xFF\xFF\x7F'
MAGIC_1 = 11111111
MAGIC_2 = 22222222
MAGIC_3 = 33333333
MAGIC_5 = 55555555
MAGIC_6 = 66666666
MAGIC_7 = 77777777
MAGIC_8 = 88888888
MAGIC_9 = 99999999
MAGIC_12 = 12121212
MAGIC_32 = 32323232
CATCH_EXCEPTIONS = (ValueError, EOFError)
S_SEC_BASE = Struct("<IQ")
    MIN_SIZE = 12 # 4b (magic) + 8b (data_size)
class ErrorPosition(namedtuple('_ErrorPosition', ['start', 'error'])):
class BytesModel(object):
class Section(BytesModel):
class DstBytes(object):
    def __repr__(self):
    def get(cls, ex):
    def first(cls, ex):
    def maybe(cls, dbytes, **kw):
    def iter_n_maybe(cls, dbytes, n, **kw):
    def lazy_n_maybe(cls, dbytes, n, *, start_pos=None, **kw):
    def __init__(self, dbytes=None, **kw):
    def read(self, dbytes, *, seek_end=True, **kw):
    def _read(self, dbytes):
    def _init_defaults(self):
    def _after_init(self):
    def write(self, dbytes, **kw):
    def visit_write(self, dbytes):
    def __repr__(self):
    def _repr_detail(self):
    def print(self, file=None, flags=(), p=None):
    def visit_print(self, p):
    def _print_type(self, p):
    def _print_offset(self, p):
    def _visit_print_data(self, p):
    def _visit_print_children(self, p):
    def base(cls, *args, **kw):
    def __init__(self, *args, **kw):
    def _init_from_args(self, *args, any_version=False, base=None, **kw):
    def __repr__(self):
    def __getitem__(self, key):
    def to_key(self, noversion=False):
    def from_key(cls, key):
    def has_version(self):
    def _read(self, dbytes):
    def _write_header(self, dbytes):
    def _print_type(self, p):
    def _print_offset(self, p):
    def __init__(self, file):
    def __repr__(self):
    def in_memory(cls):
    def from_data(cls, data):
    def from_arg(cls, arg):
    def _write_arg(cls, obj, arg, write_mode='wb'):
    def __enter__(self):
    def __exit__(self, exc_type, exc_value, exc_traceback):
    def read_bytes(self, n):
    def read_byte(self):
    def read_var_int(self):
    def read_uint(self):
    def read_int(self):
    def read_ulong(self):
    def read_long(self):
    def read_struct(self, st):
    def read_str(self):
    def read_id(self):
    def write_bytes(self, data):
    def write_uint(self, value):
    def write_int(self, value):
    def write_ulong(self, value):
    def write_long(self, value):
    def write_var_int(self, value):
    def require_equal_uint4(self, expect):
    def write_str(self, s):
    def write_id(self, id_):
    def stable_iter(self, source, *, start_pos=None):
        def gen():
    def write_size(self):
    def write_num_subsections(self):
    def write_section(self, *args, **kw): class PrintContext(object):

    """Context class for printing objects."""

    def __init__(self, file=sys.stdout, flags=()):
        self.file = file
        self.flags = flags
        # Data for each level of tree_children():
        # 0. Buffered lines for that level.
        # 1. Whether the child on that level has been ended
        #    by a call to tree_next_child().
        # 2. Counter of remaining children if 'count' was passed to
        #    tree_children() on that level.
        self._tree_data = [], [], []

    @classmethod
    def for_test(cls, file=None, flags=None):
        if flags is None:
            class ContainsEverything:
                def __contains__(self, obj):
                    return True
            flags = ContainsEverything()
        p = cls(file=file, flags=flags)
        def print_exc(e):
            raise e
        p.print_exception = print_exc
        return p

    def __call__(self, text):
        buf, ended, remain = self._tree_data
        if buf:
            count = remain[-1]
            if count is not None:
                self._tree_push_up(len(buf) - 1, [text], count <= 1)
            else:
                lines = buf[-1]
                if ended[-1]:
                    self._tree_push_up(len(buf) - 1, lines, False)
                    lines.clear()
                lines.extend(text.split('\n'))
        else:
            f = self.file
            if f is not None:
                print(text, file=f)

    def _tree_push_up(self, level, lines, last):
        while True:
            if not lines:
                return
            buf, ended, remain = self._tree_data
            if level < 0:
                raise IndexError
            was_ended = ended[level]
            ended[level] = False
            if level > 0:
                upbuffer = buf[level - 1]
                push_line = upbuffer.append
            else:
                f = self.file
                def push_line(line):
                    if f is not None:
                        print(line, file=f)
            it = iter(lines)
            if was_ended:
                if last:
                    prefix = "└─ "
                else:
                    prefix = "├─ "
                push_line(prefix + next(it))
            if last:
                prefix = "   "
            else:
                prefix = "│  "
            for line in it:
                push_line(prefix + line)

            if remain is not None:
                lines.clear()
            if level > 0 and remain[level - 1] is not None:
                # In unbuffered mode (with 'count' passed to tree_children)
                # we iterate up to the root and print everything immediately.
                level -= 1
                lines = upbuffer
                last = remain[level] <= 1
            else:
                return

    @contextmanager
    def tree_children(self, count=None):
        buf, ended, remain = self._tree_data
        level = len(buf)
        if remain and remain[level - 1] is None:
            # We are nested inside a tree_children() without count. Cannot
            # use unbufferd printing.
            count = None
        lines = []
        buf.append(lines)
        # When unbuffered, we start with ended state, so we get our tree
        # printed on the first nested line.
        ended.append(count is not None)
        remain.append(count)
        broken = False
        try:
            yield
        except BrokenPipeError:
            broken = True
            raise
        finally:
            ended[level] = True
            if not broken and count is None:
                self._tree_push_up(level, lines, True)
            buf.pop()
            ended.pop()
            remain.pop()

    def tree_next_child(self):
        buf, ended, remain = self._tree_data
        if buf:
            count = remain[-1]
            if count is not None:
                # We don't count down if nothing was printed.
                # For our tree to look correct, classes need to make sure
                # they print at least once for each child anyways.
                if not ended[-1]:
                    remain[-1] = count - 1
                    ended[-1] = True
            elif buf[-1]:
                ended[-1] = True

    def print_object(self, obj):
        obj.print(p=self)

    def print_exception(self, exc):
        exc_str = traceback.format_exception(type(exc), exc, exc.__traceback__)
        for part in exc_str:
            if part.endswith('\n'):
                part = part[:-1]
            for line in part.split('\n'):
                self(line)
        try:
            self(f"Exception start: 0x{exc.start_pos:08x}")
            self(f"Exception pos:   0x{exc.exc_pos:08x}")
        except AttributeError:
            pass class ObjectFragment(Fragment):

    """Common fragment providing the basics of any object.

    Attributes
    ----------
    real_transform : Transform
        The transform of the object.
    children : list of BaseObject
        The children of the object.
    has_children : bool
        Whether the object has a list of children. If True, a list of children
        is written even if it is empty.

    """

    __slots__ = ('real_transform', 'has_children', 'children',
                 '_child_prober')

    base_container = Section.base(Magic[3], 1)
    container_versions = 0

    def _init_defaults(self):
        super()._init_defaults()
        self.real_transform = Transform()
        self.has_children = False
        self.children = ()

    def _read(self, dbytes, *args, **kw):
        self._child_prober = getattr(self.classes, kw['child_prober'])
        Fragment._read(self, dbytes, *args, **kw)

    def _read_section_data(self, dbytes, sec):
        has_children = False
        children = ()
        if sec.content_size >= TRANSFORM_MIN_SIZE:
            transform = Transform.read_from(dbytes)
            if dbytes.tell() + Section.MIN_SIZE < sec.end_pos:
                s5 = Section(dbytes, seek_end=False)
                has_children = True
                children = self._child_prober.lazy_n_maybe(
                    dbytes, s5.count, start_pos=s5.content_start)
        else:
            transform = Transform()
        self.real_transform = transform
        self.has_children = has_children
        self.children = children

    def _visit_write_section_data(self, dbytes, sec):
        transform = self.real_transform
        children = self.children
        has_children = self.has_children or children
        if transform or has_children:
            transform.write_to(dbytes)
        if has_children:
            with dbytes.write_section(Magic[5]):
                for obj in children:
                    yield obj.visit_write(dbytes)

    def clone(self):
        """ObjectFragments cannot be cloned."""
        raise NotImplementedError("Cannot clone object") class BaseObject(Fragment):

    """Represents data within a ``Magic[6]`` Section.

    Contains a list of fragments exposed by the `fragments` property.

    Fragments can also be accessed by tag using the subscript notation::

        frag = obj['Animator']
        obj['Animator'] = anim_frag
        del obj['Animator']
        has_anim = 'Animator' in frag

    These also verify that accessed fragments are implemented and support the
    present version. The methods `get_any` and `has_any` don't perform this
    check.

    Practically all instances of this class contain an `ObjectFragment` that
    provides the `children`, and `real_transform` properties.

    Attributes
    ----------
    has_children : bool
        Whether this object has a list of children. If True, a list of children
        is written even if empty.

    Class Attributes
    ----------------
    child_class_name : str
        Name of the prober to use to find the classes of `children`.
    is_object_group : bool
        True if this is a Group object, False otherwise.
    has_children : bool
        Default value for the `has_children` instance attribute. Set to True to
        force writing of the list of children even if empty.
    default_transform : Transform
        The default transform of this class. Required to use the `transform`
        property.

    """

    type = None
    child_classes_name = 'base_objects'
    is_object_group = False
    has_children = False

    default_transform = None

    @classproperty
    def class_tag(cls):

        """The tag of this class.

        Defaults to the `type` attribute specified by the class.

        """

        return cls.type

    def __init__(self, *args, **kw):
        container = kw.get('container')
        if container is not None:
            self.type = container.type
        super().__init__(*args, **kw)

    def clone(self):
        """BaseObjects cannot be cloned."""
        raise NotImplementedError("Cannot clone object")

    real_transform = _object_property(
        'real_transform', default=Transform(),
        doc="The transform as read from the file.")

    children = _object_property(
        'children', default=(),
        doc="The list of contained children.")

    @property
    def transform(self):
        """The *effective* transform.

        Like `real_transform`, but always returns an *effective* transform.

        The `default_transform` value needs to be set to the object-specific
        default transform for this to work.

        Raises
        ------
        NoDefaultTransformError
            If this class doesn't specify its default transform with the
            `default_transform` attribute.

        """
        t = self.real_transform
        defs = self.default_transform
        if defs is None:
            raise NoDefaultTransformError(
                f"default transform unknown for {self.type!r}")
        return t.effective(*defs)

    @transform.setter
    def transform(self, value):
        if not isinstance(value, Transform):
            value = Transform(*value)
        if value:
            default = self.default_transform
            if default:
                value = value.strip(*default)
        self.real_transform = value

    fragments = property(attrgetter('_fragments'),
                         doc="The list of contained fragments.")

    @fragments.setter
    def fragments(self, value):
        self._sections = _FragmentsContainerView(value)
        self._fragments = value

    sections = property(attrgetter('_sections'),
                        doc=("Containers of the fragments of this object."
                             " (read-only view of fragments*.container)"))

    def fragment_by_type(self, typ):

        """Get fragment by class.

        Not recommended. Use ``self[tag]`` instead.

        """

        probe = self.classes.fragments.probe_section
        i = 0
        for sec in self.sections:
            sec_typ = probe(sec)
            if issubclass(sec_typ, typ):
                return self.fragments[i]
            i += 1
        return None

    def filter_fragments(self, section_pred):

        """Iterate fragments filtered by section.

        This is useful to reduce work when iterating fragments, as unselected
        fragments aren't instantiated.

        Parameters
        ----------
        section_pred : function (Section) -> bool
            The predicate function. Returns True if the fragment should be
            yielded, False otherwise.

        Yields
        ------
        fragment : Fragment
            Fragments of the sections for which `section_pred` returned True.

        """

        fragments = self.fragments
        i = 0
        for sec in self.sections:
            if section_pred(sec):
                yield fragments[i]
            i += 1

    def __getitem__(self, tag):

        """Get a fragment by tag, if implemented.

        Provides ``self[tag] -> Fragment``.

        Parameters
        ----------
        tag : str
            The tag of the fragment to get.

        Returns
        -------
        fragment : Fragment
            The fragment with the given tag.

        Raises
        ------
        FragmentKeyError
            If fragment is not present or not implemented. Use the exception's
            `is_present` attribute to check for the reason. Importing
            `FragmentKeyError` is in most cases unnecessary, as it extends
            `KeyError`.
        TagError
            If the given tag is not registered.

        """

        base_key, versions = self.classes.fragments._get_tag_impl_info(tag)
        i = 0
        for sec in self.sections:
            if sec.to_key(noversion=True) == base_key:
                break
            i += 1
        else:
            raise FragmentKeyError(tag)

        fragments = self.fragments

        # We always allow getting the fragment if it has the same tag,
        # regardless of whether we have a registered implementation.
        # This allows retrieving it after assignment via __setitem__.
        peeked = LazyMappedSequence.peek(fragments, i)
        if peeked is not UNSET:
            if getattr(peeked, 'class_tag', None) != tag:
                raise FragmentKeyError(tag, sec.version)
            return peeked

        if (versions != 'all' and sec.has_version()
                and sec.version not in versions):
            raise FragmentKeyError(tag, sec.version)

        return fragments[i]

    def __setitem__(self, tag, frag):

        """Add or replace a fragment by tag.

        Provides ``self[tag] = frag``.

        Verifies that the tag and fragment's container match.

        Parameters
        ----------
        tag : str
            The tag of the fragment.
        frag : Fragment
            The fragment.

        Raises
        ------
        KeyError
            If the tag doesn't match the fragment's container.
        TagError
            If the given tag is not registered.

        """

        base_key = self.classes.fragments.get_base_key(tag)
        if frag.container.to_key(noversion=True) != base_key:
            raise KeyError(f"Invalid fragment container: expected"
                           f" {base_key!r} but got {frag.container!r}")
        ftag = frag.class_tag
        if ftag != tag:
            raise KeyError(f"Invalid fragment tag: fragment tag is {ftag!r}"
                           f" but expected {tag!r}")
        frags = list(self.fragments)
        i = 0
        for sec in self.sections:
            if sec.to_key(noversion=True) == base_key:
                frags[i] = frag
                break
            i += 1
        else:
            frags.append(frag)
        self.fragments = frags

    def __delitem__(self, tag):

        """Remove a fragment by tag.

        Provides ``del self[tag]``.

        Parameters
        ----------
        tag : str
            The tag of the container of fragment to remove.

        Raises
        ------
        KeyError
            If there is no fragment with the given tag.
        TagError
            If the given tag is not registered.

        """

        base_key = self.classes.fragments.get_base_key(tag)
        i = 0
        for sec in self.sections:
            if sec.to_key(noversion=True) == base_key:
                frags = list(self.fragments)
                del frags[i]
                self.fragments = frags
                break
            i += 1
        else:
            raise KeyError(f"Fragment with tag {tag!r} is not present")

    def __contains__(self, tag):

        """Check whether a fragment with given tag is present and implemented.

        Provides ``tag in self``.

        Parameters
        ----------
        tag : str
            The tag to check.

        Returns
        -------
        implemented : bool
            Whether a fragment with given tag is present and implemented.

        Raises
        ------
        TagError
            If the given tag is not registered.

        """

        base_key, versions = self.classes.fragments._get_tag_impl_info(tag)
        i = 0
        for sec in self.sections:
            if sec.to_key(noversion=True) == base_key:
                break
            i += 1
        else:
            return False

        # Peek operation analogous to __getitem__.
        peeked = LazyMappedSequence.peek(self.fragments, i)
        if peeked is not UNSET:
            return peeked.class_tag == tag

        return (versions == 'all' or not sec.has_version()
                or sec.version in versions)

    def __iter__(self):
        "Not iterable. Implemented only to prevent __getitem__ iteration."
        raise TypeError(f"{type(self).__name__!r} object is not iterable")

    def get_any(self, tag):

        """Get fragment with given tag, regardless of implementation.

        Parameters
        ----------
        tag : str
            The tag of the fragment to get.

        Returns
        -------
        fragment : Fragment or None
            The fragment with the given tag, or None if there is no fragment
            with the given tag.

        Raises
        ------
        TagError
            If the given tag is not registered.

        """

        base_key = self.classes.fragments.get_base_key(tag)
        i = 0
        for sec in self.sections:
            if sec.to_key(noversion=True) == base_key:
                return self.fragments[i]
            i += 1
        return None

    def has_any(self, tag):

        """Check whether a fragment with given tag is present.

        Parameters
        ----------
        tag : str
            The tag of the fragment.

        Returns
        -------
        present : bool
            Whether there is any fragment with the given tag.

        Raises
        ------
        TagError
            If the given tag is not registered.

        """

        base_key = self.classes.fragments.get_base_key(tag)
        for sec in self.sections:
            if sec.to_key(noversion=True) == base_key:
                return True
        return False

    def _read_section_data(self, dbytes, sec):
        self.type = sec.type
        self._sections = Section.lazy_n_maybe(dbytes, sec.count)
        self._fragments = LazyMappedSequence(
            self._sections, self._read_fragment)

    def _read_fragment(self, sec):
        if sec.exception:
            # failed to read section - return object containing the error
            return Fragment(exception=sec.exception)
        dbytes = self.dbytes
        dbytes.seek(sec.content_start)
        classes = self.classes
        return classes.fragments.maybe(
            dbytes, probe_section=sec,
            child_prober=self.child_classes_name,
            classes=classes)

    def _get_write_section(self, sec):
        try:
            cid = sec.id
        except AttributeError:
            cid = None
        return Section(Magic[6], self.type, id=cid)

    def _visit_write_section_data(self, dbytes, sec):
        for frag in self.fragments:
            yield frag.visit_write(dbytes)

    def _init_defaults(self):
        super()._init_defaults()
        sections = [sec for sec in default_fragments.get_sections(self)
                    if sec is not None]
        fragments = []
        for sec in sections:
            cls = self.classes.fragments.probe_section(sec)
            frag = cls(container=sec)
            if cls is ObjectFragment:
                frag.has_children = self.has_children
            fragments.append(frag)
        self.fragments = fragments

    def _repr_detail(self):
        supstr = super()._repr_detail()
        return f" type={self.type!r}{supstr}"

    def _print_type(self, p):
        p(f"Object type: {self.type!r}")

    def _visit_print_data(self, p):
        yield super()._visit_print_data(p)
        if 'transform' in p.flags:
            p(f"Transform: {format_transform(self.real_transform)}")
        if 'fragments' in p.flags and self.fragments:
            if 'allprops' in p.flags:
                p(f"Fragments: {len(self.fragments)}")
                with p.tree_children(len(self.fragments)):
                    for frag in self.fragments:
                        p.tree_next_child()
                        yield frag.visit_print(p)
            else:
                pred = self.classes.fragments.is_section_interesting
                frags = self.filter_fragments(pred)
                try:
                    frag = next(frags)
                except StopIteration:
                    pass
                else:
                    p(f"Fragments: {len(self.fragments)} <filtered>")
                    with p.tree_children():
                        p.tree_next_child()
                        yield frag.visit_print(p)
                        for frag in frags:
                            p.tree_next_child()
                            yield frag.visit_print(p)

    def _visit_print_children(self, p):
        num = len(self.children)
        if num:
            p(f"Children: {num}")
            with p.tree_children(num):
                for obj in self.children:
                    p.tree_next_child()
                    yield obj.visit_print(p) class Fragment(BytesModel):

    """Represents a container section and its content.

    Attributes
    ----------
    dbytes : DstBytes
        The source this fragment has been read from.
    container : Section
        The container section of this fragment.
    classes : ClassesRegistry
        The registry used for contained objects and tags. Default is the
        ``DefaultRegistry``.

    """

    __slots__ = ('_raw_data', 'container', 'dbytes', 'classes')

    default_container = None

    @classproperty
    def base_container(cls):

        """The base container of this class.

        Defaults to None, or the base section of `default_container` if set.

        """

        con = cls.default_container
        if con is None:
            return None
        if con.has_version():
            return Section(con, version=None)
        return con

    @classproperty
    def container_versions(cls):

        """The container versions supported by this class.

        Defaults to None, or the version of `default_container` if it has a
        version.

        """

        con = cls.default_container
        if con is None or not con.has_version():
            return None
        return con.version

    @classmethod
    def get_default_container(cls):

        """Get the default container when creating this class.

        Returns
        -------
        container : Section
            `default_container` if it is not None.

            If `default_container` is None, returns `base_container`, with the
            highest version in `container_versions` if the section has a
            version and both are not None. If it has no version, it is returned
            as-is. Otherwise, returns None.

        """

        defcon = cls.default_container
        if defcon is not None:
            return defcon
        base = cls.base_container
        if base is None:
            return None
        if not base.has_version():
            return base
        versions = cls.container_versions
        if versions is None:
            return None
        if isinstance(versions, numbers.Integral):
            version = versions
        else:
            version = max(versions)
        return Section(base, version=version)

    is_interesting = False

    @classproperty
    def class_tag(cls):

        """Tag of this class.

        Default is the class name with "Fragment" suffix removed, and raises
        AttributeError if the name does not end with "Fragment".

        """

        name = cls.__name__
        if not name.endswith('Fragment') or name == 'Fragment':
            raise AttributeError(f"Could not get class tag for {cls!r}")
        return name[:-8]

    def __init__(self, dbytes=None, **kw):
        self.classes = kw.pop('classes', DefaultClasses)
        super().__init__(dbytes=dbytes, **kw)

    def _init_defaults(self):
        super()._init_defaults()
        con = self.get_default_container()
        if con is not None:
            self.container = con

    def _read(self, dbytes, *, container=None, classes=None, child_prober=None):

        """Read data of the Fragment.

        Parameters
        ----------
        container : Section
            Container Section of this Fragment. `dbytes` needs to be positioned
            at `content_start` of the section if set. If omitted, it is read
            from `dbytes` before reading its content.
        classes : ClassesRegistry
            If not None, it is first assigned to the `classes` attribute of the
            object.
        child_prober : str
            Specifies the child object prober name for ObjectFragment. Only
            accepted here to allow passing it without checking for this
            object's type.

        """

        self.dbytes = dbytes
        if classes is not None:
            self.classes = classes
        if container is None:
            container = Section(dbytes, seek_end=False)
        self.container = container
        self.end_pos = container.end_pos
        self._read_section_data(dbytes, container)

    def visit_write(self, dbytes):
        con = getattr(self, 'container', None)
        sec = self._get_write_section(con)
        with dbytes.write_section(sec):
            yield self._visit_write_section_data(dbytes, sec)

    def _get_write_section(self, sec):
        return sec or self.get_default_container()

    @property
    def raw_data(self):

        """The unmodified raw bytes of this fragment.

        Only valid for fragments that have been read or if this property has
        been assigned first.

        This attribute is initialized lazily. File errors may occur on access.

        Returns
        -------
        data : bytes
            The bytes of this fragment.

        Raises
        ------
        AttributeError
            If this fragment has no assigned raw data and has no source and
            container information to initialize it.

        """

        try:
            return self._raw_data
        except AttributeError:
            pass
        try:
            dbytes = self.dbytes
            sec = self.container
            start = sec.content_start
            size = sec.content_size
        except AttributeError:
            raise AttributeError(f"Missing source and/or container information")
        with dbytes:
            dbytes.seek(start)
            data = dbytes.read_bytes(size)
            self._raw_data = data
        return data

    @raw_data.setter
    def raw_data(self, value):
        self._raw_data = value

    def clone(self):

        """Clones the fragment.

        Subclasses should override `_clone_data` to copy values to the new
        instance. The default just copies `raw_data`.

        The container is always copied, but only its type information is
        retained.

        """

        new = type(self)()
        try:
            con = self.container
        except AttributeError:
            pass
        else:
            new.container = Section(con)
        self._clone_data(new)
        return new

    def _clone_data(self, new):
        new.raw_data = self.raw_data

    def _read_section_data(self, dbytes, sec):
        """Read data of the given section."""

    def _visit_write_section_data(self, dbytes, sec):

        """Write data of the given section (trampolined).

        Default implementation delegates to `_write_section_data`.

        """

        self._write_section_data(dbytes, sec)
        return
        yield

    def _write_section_data(self, dbytes, sec):

        """Non-trampolined version of `_write_section_data`.

        The default implementation just writes this object's `raw_data`.

        """

        dbytes.write_bytes(self.raw_data)

    def __str__(self):
        from io import StringIO
        sio = StringIO()
        flags = ('allprops', 'sections', 'transform', 'offset',
                 'objects', 'subobjects', 'groups', 'fragments')
        self.print(file=sio, flags=flags)
        return sio.getvalue()

    def _print_type(self, p):
        hint = ''
        try:
            tag = self.class_tag
        except AttributeError:
            tag = None
        if tag is None:
            type_str = 'Unknown'
        else:
            type_str = repr(tag)

        actual_str = ''
        ver_str = ''
        overridden = False
        try:
            container = self.container
        except AttributeError:
            pass
        else:
            try:
                con_tag = self.classes.fragments.get_tag(container)
            except KeyError:
                con_tag = None
            if con_tag != tag:
                overridden = tag is not None
                if con_tag is None:
                    actual_str = " (container Unknown)"
                else:
                    actual_str = f" (container {con_tag!r})"

            if not overridden and tag is not None and container.has_version():
                version = self.container.version
                ver_str = f" version {version!r}"

        if overridden:
            hint = ' [overridden]'
        elif tag is None:
            hint = ' [dummy]'
        else:
            hint = ''

        p(f"Fragment: {type_str}{actual_str}{ver_str}{hint}")

    def _visit_print_data(self, p):
        if 'sections' in p.flags:
            try:
                container = self.container
            except AttributeError:
                pass
            else:
                p(f"Container:")
                with p.tree_children(1):
                    yield container.visit_print(p) class GoldenSimple(LevelObject):

    default_transform = Transform.fill()

    def _init_defaults(self):
        super()._init_defaults()
        material_attrs.reset_colors(self) class GoldenSimplesFragment(BaseConstructFragment):

    base_container = Section.base(Magic[2], 0x83)
    container_versions = 3

    _construct_ = Struct(
        'image_index' / Default(UInt, 17),
        'emit_index' / Default(UInt, 17),
        'preset' / Default(UInt, 0),
        'tex_scale' / Default(Float[3], (1, 1, 1)),
        'tex_offset' / Default(Float[3], (0, 0, 0)),
        'flip_tex_uv' / Default(Byte, 0),
        'world_mapped' / Default(Byte, 0),
        'disable_diffuse' / Default(Byte, 0),
        'disable_bump' / Default(Byte, 0),
        'bump_strength' / Default(Float, 0),
        'disable_reflect' / Default(Byte, 0),
        'disable_collision' / Default(Byte, 0),
        'additive_transp' / Default(Byte, 0),
        'multip_transp' / Default(Byte, 0),
        'invert_emit' / Default(Byte, 0),
    ) class ClassCollection(_BaseProber, ClassCollector):

    "Collection and Prober of registered classes."

    def __init__(self, *, key=None, get_fallback_container=None, **kw):
        super().__init__(**kw)
        self.key = key
        self.get_fallback_container = get_fallback_container
        # We don't have funcs on ClassCollector because they don't need to be
        # lazy-loaded and there's no use for it yet.
        self._funcs_by_tag = OrderedDict()
        self._funcs = self._funcs_by_tag.values()

    def add_func(self, func, tag):

        """Register a function as probe fallback.

        The function is called by passing it the probed section. The function
        returns the class to use for the section, or None to proceed to the
        next fallback.

        Parameters
        ----------
        func : function (Section) -> cls or None
            The function to register.
        tag : str
            A tag for the function. If a function with given tag exists, it is
            replaced with `func`.

        """

        self._funcs_by_tag[tag] = func

    def func(self, tag):
        "Decorator style method for `add_func`."
        def decorate(func):
            self.add_func(func, tag)
            return func
        if callable(tag):
            raise TypeError("target passed as tag")
        return decorate

    def _probe_section_key(self, key):
        cls = self._sections.get(key)
        if cls is not None:
            return cls
        module = self._autoload_sections.get(key)
        if module is not None:
            self._autoload_impl_module(key, module)
            return self._sections.get(key)
        return None

    def _probe_fallback(self, sec):
        for func in self._funcs:
            cls = func(sec)
            if cls is not None:
                return cls
        return None

    def get_base_key(self, tag):

        """Get the base section key for the given tag.

        Parameters
        ----------
        tag : str
            The tag of the section.

        Returns
        -------
        base_key : see Section.to_key(noversion=True)
            The base section key of the tag.

        Raises
        ------
        TagError
            If the given tag is not registered.
        TypeError
            If there is no container information for the given tag.

        """

        try:
            info = self._classes[tag]
        except KeyError:
            raise TagError.check(tag)
        try:
            base = info['base_container']
        except KeyError:
            raise TypeError(f"No container information for tag {tag!r}")
        return base

    def get_tag(self, section):

        """Get the tag of the given section.

        Parameters
        ----------
        section : Section
            The section.

        Returns
        -------
        tag : str
            The tag of the section.

        Raises
        ------
        KeyError
            If there is no tag information for the given section.

        """

        key = section.to_key(noversion=True)
        return self._tags_by_base_key[key]

    def _get_tag_impl_info(self, tag):
        try:
            info = self._classes[tag]
        except KeyError:
            raise TagError.check(tag)
        try:
            base = info['base_container']
        except KeyError:
            raise TypeError(f"Class with tag {tag!r} has no container information")
        if info.get('noversion_cls') is not None:
            versions = 'all'
        else:
            versions = info.get('versions')
        return base, versions

    def fragment_attrs(self, *tags):

        """Decorate object with fragment attribute properties.

        Creates a `fragment_property` for each entry in the `_fields_` dict of
        each specified fragment.

        Each fragment is also added to the `default_fragments` of the given
        object.

        Parameters
        ----------
        tags : any number of str
            The tags of fragment classes to add class attributes for.

        Raises
        ------
        TagError
            If a given tag is not registered.
        TypeError
            If there is no field information for a given tag.

        """

        def decorate(cls):
            from .base import default_fragments
            containers = []
            for tag in tags:
                try:
                    info = self._classes[tag]
                except KeyError:
                    raise TagError.check(tag)
                try:
                    fields = info['fields']
                except KeyError:
                    raise TypeError(f"No field information for tag {tag!r}")
                container_key = info.get('base_container', None)
                if container_key is not None:
                    container = Section.from_key(container_key)
                    if container.has_version():
                        versions = info.get('versions', None)
                        if versions is not None:
                            container.version = max(versions)
                    containers.append(container)
                for name, default in fields.items():
                    setattr(cls, name, fragment_property(tag, name, default))
            default_fragments.add_sections_to(cls, *containers)
            return cls
        return decorate

    def __klass(self, tag, version, fallback):
        if fallback is None:
            fallback = self.get_fallback_container is not None
        elif fallback and self.get_fallback_container is None:
            raise ValueError("Fallback not defined for this collection")

        try:
            info = self._classes[tag]
        except KeyError:
            if not fallback:
                raise TagError.check(tag)
            return self.baseclass, self.get_fallback_container(tag)

        try:
            (modname, clsname), def_version = _get_klass_def(info, version)
        except KeyError:
            raise ClassLookupError(tag, version=version)
        container = info.get('base_container')
        if container is not None:
            container = Section.from_key(container)
            if def_version is not None and container.has_version():
                container.version = def_version
        mod = importlib.import_module(modname)
        return getattr(mod, clsname), container

    def klass(self, tag, *, version=None):

        """Get a registered class by tag.

        Note that the returned class does not necessarily only create objects
        of the given tag. To safely create objects of a given tag, the
        `factory` or `create` methods should be used.

        Parameters
        ----------
        tag : str
            The tag of the class.
        version : int
            Specify which version to get. If None, the highest version is used.
            Useful for fragments, as objects are not versioned.

        Returns
        -------
        cls : class
            The registered class.

        Raises
        ------
        TagError
            If the given tag is not registered.
        ClassLookupError
            If the tag is registered, but no matching class is available.

        """

        return self.__klass(tag, version, False)[0]

    def factory(self, tag, *, version=None, fallback=None):

        """Get a factory that creates instances of the class of a given tag.

        Parameters
        ----------
        tag : str
            The tag of the class.
        version : int
            The version of the class to create. If None, the highest version is
            used. Useful for fragments, as objects are not versioned.
        fallback : bool
            Whether a fallback should be used to create unknown classes. If
            None, this defaults to True if a fallback is defined for this
            collection.

        Returns
        -------
        factory : callable (**) -> object
            Factory that creates objects of the given tag. Keyword arguments
            passed to the factory are passed to the class's constructor.

        Raises
        ------
        TagError
            If the given tag is not registered and no fallback is used.
        ClassLookupError
            If the tag is registered, but no matching class is available.

        Notes
        -----
        Fallbacks are defined for `level_object` and `level_subobject`
        categories. This allows creating unimplemented objects, for which only
        the baseclass functionality is provided.

        """

        cls, container = self.__klass(tag, version, fallback)
        return _InstanceFactory(cls, container)

    def create(self, tag, *, fallback=None, **kw):

        """Create an instance of the class of a given tag.

        Behaves like `factory`, creating a new instance immediately.

        Parameters
        ----------
        See `factory` method. `version` cannot be specified here.

        Returns
        -------
        obj : cls
            The created object.

        Raises
        ------
        See `factory` method.

        """

        cls, container = self.__klass(tag, None, fallback)
        if 'container' not in kw and 'dbytes' not in kw:
            kw['container'] = container
        return cls(**kw)

    def is_section_interesting(self, sec):

        """Check for interestingness of a section.

        Checks whether any class registered for the base key of the given
        section has an `is_interesting` class attribute set to True.

        """

        return sec.to_key(noversion=True) in self._interesting_sections

    def print_listing(self, file=None, *, tag=None, p=None):

        """Print a listing of registered classes.

        Parameters
        ----------
        file : file
            The file to print to.
        p : PrintContext
            The print context to use.

        """

        if file is None:
            file = sys.stdout
        if p is None:
            p = PrintContext(file=file)

        def print_info(tag, info):
            p(f"Tag: {tag!r}")
            base_key = info.get('base_container')
            if base_key is not None:
                p(f"Base container: {Section.from_key(base_key)!r}")
            vers = [repr(ver) for ver in sorted(info.get('versions', {}))]
            if info.get('noversion_cls') is not None:
                vers.append('any')
            if vers:
                vers_str = ', '.join(vers)
                p(f"Versions: {vers_str}")
            else:
                p(f"Versions: none")
            fields = info.get('fields')
            if fields:
                p(f"Fields: {len(fields)}")
                with p.tree_children(len(fields)):
                    for name, def_value in fields.items():
                        p.tree_next_child()
                        p(f"Field: {name} = {def_value!r}")

        if tag is not None:
            try:
                info = self._classes[tag]
            except KeyError:
                raise TagError.check(tag)
            print_info(tag, info)
        else:
            p(f"Base class: {self.baseclass.__module__}.{self.baseclass.__qualname__}")
            if self._funcs_by_tag:
                p(f"Functions: {len(self._funcs_by_tag)}")
                with p.tree_children(len(self._funcs_by_tag)):
                    for tag in self._funcs_by_tag:
                        p(f"Function: {tag!r}")
            p(f"Tags: {len(self._classes)}")
            with p.tree_children(len(self._classes)):
                for tag, info in sorted(self._classes.items()):
                    p.tree_next_child()
                    print_info(tag, info)

    def _load_autoload_content(self, content):
        self._autoload_sections.update(content['sections'])
        self._interesting_sections.update(content['interesting'])
        self._classes.update(content['classes'])
        self._tags_by_base_key.update(content['key_tags'])

    def _generate_autoload_content(self):
        return {
            'sections': {key: cls.__module__
                         for key, cls in self._sections.items()},
            'interesting': set(self._interesting_sections),
            'classes': dict(self._classes),
            'key_tags': dict(self._tags_by_base_key),
        }

    def _get_current_autoload_content(self):
        return {
            'sections': dict(self._autoload_sections),
            'interesting': set(self._interesting_sections),
            'classes': dict(self._classes),
            'key_tags': dict(self._tags_by_base_key),
        }

    def _autoload_impl_module(self, sec_key, impl_module):
        mod = importlib.import_module(impl_module)
        coll = getattr(mod.Classes, self.key)
        self._load_impl(coll, False)

    def _load_impl(self, coll, update_classes):
        self._sections.update(((k, v) for k, v in coll._sections.items()
                               if k not in self._sections))
        if update_classes:
            _update_class_info(self._classes, coll._classes)
            self._interesting_sections.update(coll._interesting_sections)
            self._tags_by_base_key.update(coll._tags_by_base_key) class TagError(LookupError):
    "The given tag is not registered."

    @classmethod
    def check(cls, tag):
        "Create exception instance, but check type of tag first."
        if not isinstance(tag, str):
            return TypeError(f"tag must be str, not {type(tag).__name__}")
        return cls(tag)

    def __init__(self, tag):
        LookupError.__init__(self, repr(tag)) def write_read(obj, read_func=None, do_check_exceptions=True):
    if read_func is None:
        read_func = type(obj)

    dbytes = DstBytes.in_memory()

    obj.write(dbytes)
    dbytes.seek(0)
    disable_writes(dbytes)
    result = read_func(dbytes)

    if do_check_exceptions:
        check_exceptions(result)

    return result, dbytes.file def check_exceptions(obj):
    if obj.exception:
        raise obj.exception
    if isinstance(obj, Level):
        check_exceptions(obj.settings)
        for layer in obj.layers:
            check_exceptions(layer)
    if isinstance(obj, BaseObject):
        for frag in obj.fragments:
            check_exceptions(frag)
        for child in obj.children:
            check_exceptions(child)
    if isinstance(obj, Layer):
        for obj in obj.objects:
            check_exceptions(obj) @contextmanager
def small_stack(size):
    saved_limit = sys.getrecursionlimit()
    sys.setrecursionlimit(size + len(inspect.stack()))
    try:
        yield
    finally:
        sys.setrecursionlimit(saved_limit)
import unittest
from io import StringIO, BytesIO
from distance import DefaultClasses
from distance.bytes import Magic, Section, DstBytes
from distance.printing import PrintContext
from distance.base import (
    ObjectFragment,
    BaseObject,
    Fragment,
)
from distance._impl.level_objects.objects import (
    GoldenSimple,
)
from distance._impl.fragments.levelfragments import (
    GoldenSimplesFragment,
)
from distance.classes import ClassCollection, TagError
from .common import write_read, check_exceptions, small_stack





def TagFragment(name, tag, **kw):
    kw['class_tag'] = tag
    return type(name, (Fragment,), kw)


class CustomGSFragment(Fragment):
    class_tag = 'GoldenSimples'
    default_container = Section(base=GoldenSimplesFragment.base_container, version=9001)


class BaseObjectTest(unittest.TestCase):

    def test_new_print(self):
        obj = BaseObject()
        p = PrintContext.for_test()
        p.print_object(obj)
        repr(obj)

    def test_print_deeply_nested(self):
        output = StringIO()
        p = PrintContext.for_test(file=output, flags=())
        # create deeply nested objects
        obj = BaseObject()
        for _ in range(100):
            obj = BaseObject(children=[obj])

        # reduce recursion limit so we don't need to
        # use so much time and memory
        with small_stack(50):
            p.print_object(obj)

        lines = output.getvalue().splitlines()
        self.assertEqual(len(lines), 201)
        self.assertEqual(lines[-1], ("   " * 99) + "└─ Object type: None")

    def test_write_deeply_nested(self):
        # create deeply nested objects
        obj = BaseObject(type='First')
        for _ in range(100):
            obj = BaseObject(type='Test', children=[obj])

        with small_stack(50):
            result, rdb = write_read(obj, do_check_exceptions=False)

        r = result
        count = 0
        while r.children:
            count += 1
            r = r.children[0]

        self.assertEqual(count, 100)

    def test_write_and_read_stream(self):
        buf = BytesIO()

        BaseObject(type='Test').write(buf)
        buf.seek(0)
        result = BaseObject(buf)

        self.assertEqual(result.type, 'Test')
        check_exceptions(result)

    def test_getitem_object(self):
        obj = BaseObject()

        self.assertEqual(list(obj.fragments), [obj['Object']])

    def test_getitem_goldensimples(self):
        obj = GoldenSimple(type='CubeGS')
        frag = obj['GoldenSimples']
        expect = next(f for f in obj.fragments if isinstance(f, GoldenSimplesFragment))
        self.assertEqual(frag, expect)

    def test_getitem_empty_fragments(self):
        obj = BaseObject()
        obj.fragments = []

        with self.assertRaises(KeyError) as cm:
            obj['Object']
        self.assertEqual(cm.exception.is_present, False)

    def test_getitem_unimplemented_version(self):
        coll = ClassCollection()
        coll.add_fragment(ObjectFragment)

        coll.fragment(TagFragment('Frag1', 'Test', default_container=Section(Magic[2], 20, 1)))