mutagen/_util.py
class MutagenError(Exception):
    """Base class for all custom exceptions in mutagen

    .. versionadded:: 1.25
    """

    __module__ = "mutagen"
mutagen/_util.py
def loadfile(method=True, writable=False, create=False):
    """A decorator for functions taking a `filething` as a first argument.

    Passes a FileThing instance as the first argument to the wrapped function.

    Args:
        method (bool): If the wrapped functions is a method
        writable (bool): If a filename is passed opens the file readwrite, if
            passed a file object verifies that it is writable.
        create (bool): If passed a filename that does not exist will create
            a new empty file.
    """

    def convert_file_args(args, kwargs):
        filething = args[0] if args else None
        filename = kwargs.pop("filename", None)
        fileobj = kwargs.pop("fileobj", None)
        return filething, filename, fileobj, args[1:], kwargs

    def wrap(func):

        @wraps(func)
        def wrapper(self, *args, **kwargs):
            filething, filename, fileobj, args, kwargs = \
                convert_file_args(args, kwargs)
            with _openfile(self, filething, filename, fileobj,
                           writable, create) as h:
                return func(self, h, *args, **kwargs)

        @wraps(func)
        def wrapper_func(*args, **kwargs):
            filething, filename, fileobj, args, kwargs = \
                convert_file_args(args, kwargs)
            with _openfile(None, filething, filename, fileobj,
                           writable, create) as h:
                return func(h, *args, **kwargs)

        return wrapper if method else wrapper_func

    return wrap
mutagen/id3/_util.py
class ID3NoHeaderError(error, ValueError):
    pass
mutagen/id3/_util.py
class error(MutagenError):
    pass
mutagen/_util.py
def resize_bytes(fobj, old_size, new_size, offset):
    """Resize an area in a file adding and deleting at the end of it.
    Does nothing if no resizing is needed.

    Args:
        fobj (fileobj)
        old_size (int): The area starting at offset
        new_size (int): The new size of the area
        offset (int): The start of the area
    Raises:
        IOError
    """

    if new_size < old_size:
        delete_size = old_size - new_size
        delete_at = offset + new_size
        delete_bytes(fobj, delete_size, delete_at)
    elif new_size > old_size:
        insert_size = new_size - old_size
        insert_at = offset + old_size
        insert_bytes(fobj, insert_size, insert_at)
mutagen/id3/_file.py
class ID3(ID3Tags, mutagen.Metadata):
    """ID3(filething=None)

    A file with an ID3v2 tag.

    If any arguments are given, the :meth:`load` is called with them. If no
    arguments are given then an empty `ID3` object is created.

    ::

        ID3("foo.mp3")
        # same as
        t = ID3()
        t.load("foo.mp3")

    Arguments:
        filething (filething): or `None`

    Attributes:
        version (tuple[int]): ID3 tag version as a tuple
        unknown_frames (list[bytes]): raw frame data of any unknown frames
            found
        size (int): the total size of the ID3 tag, including the header
    """

    __module__ = "mutagen.id3"

    PEDANTIC = True
    """`bool`:

    .. deprecated:: 1.28

        Doesn't have any effect
    """

    filename = None

    def __init__(self, *args, **kwargs):
        self._header = None
        self._version = (2, 4, 0)
        super(ID3, self).__init__(*args, **kwargs)

    @property
    def version(self):
        if self._header is not None:
            return self._header.version
        return self._version

    @version.setter
    def version(self, value):
        self._version = value

    @property
    def f_unsynch(self):
        if self._header is not None:
            return self._header.f_unsynch
        return False

    @property
    def f_extended(self):
        if self._header is not None:
            return self._header.f_extended
        return False

    @property
    def size(self):
        if self._header is not None:
            return self._header.size
        return 0

    def _pre_load_header(self, fileobj):
        # XXX: for aiff to adjust the offset..
        pass

    @convert_error(IOError, error)
    @loadfile()
    def load(self, filething, known_frames=None, translate=True, v2_version=4,
             load_v1=True):
        """Load tags from a filename.

        Args:
            filename (filething): filename or file object to load tag data from
            known_frames (Dict[`mutagen.text`, `Frame`]): dict mapping frame
                IDs to Frame objects
            translate (bool): Update all tags to ID3v2.3/4 internally. If you
                intend to save, this must be true or you have to
                call update_to_v23() / update_to_v24() manually.
            v2_version (int): if update_to_v23 or update_to_v24 get called
                (3 or 4)
            load_v1 (bool): Load tags from ID3v1 header if present. If both
                ID3v1 and ID3v2 headers are present, combine the tags from
                the two, with ID3v2 having precedence.

                .. versionadded:: 1.42

        Example of loading a custom frame::

            my_frames = dict(mutagen.id3.Frames)
            class XMYF(Frame): ...
            my_frames["XMYF"] = XMYF
            mutagen.id3.ID3(filename, known_frames=my_frames)
        """

        fileobj = filething.fileobj

        if v2_version not in (3, 4):
            raise ValueError("Only 3 and 4 possible for v2_version")

        self.unknown_frames = []
        self._header = None
        self._padding = 0

        self._pre_load_header(fileobj)

        try:
            self._header = ID3Header(fileobj)
        except (ID3NoHeaderError, ID3UnsupportedVersionError):
            if not load_v1:
                raise

            frames, offset = find_id3v1(fileobj, v2_version, known_frames)
            if frames is None:
                raise

            self.version = ID3Header._V11
            for v in frames.values():
                if len(self.getall(v.HashKey)) == 0:
                    self.add(v)
        else:
            # XXX: attach to the header object so we have it in spec parsing..
            if known_frames is not None:
                self._header._known_frames = known_frames

            data = read_full(fileobj, self.size - 10)
            remaining_data = self._read(self._header, data)
            self._padding = len(remaining_data)

            if load_v1:
                v1v2_ver = 4 if self.version[1] == 4 else 3
                frames, offset = find_id3v1(fileobj, v1v2_ver, known_frames)
                if frames:
                    for v in frames.values():
                        if len(self.getall(v.HashKey)) == 0:
                            self.add(v)

        if translate:
            if v2_version == 3:
                self.update_to_v23()
            else:
                self.update_to_v24()

    def _prepare_data(self, fileobj, start, available, v2_version, v23_sep,
                      pad_func):

        if v2_version not in (3, 4):
            raise ValueError("Only 3 or 4 allowed for v2_version")

        config = ID3SaveConfig(v2_version, v23_sep)
        framedata = self._write(config)

        needed = len(framedata) + 10

        fileobj.seek(0, 2)
        trailing_size = fileobj.tell() - start

        info = PaddingInfo(available - needed, trailing_size)
        new_padding = info._get_padding(pad_func)
        if new_padding < 0:
            raise error("invalid padding")
        new_size = needed + new_padding

        new_framesize = BitPaddedInt.to_str(new_size - 10, width=4)
        header = struct.pack(
            '>3sBBB4s', b'ID3', v2_version, 0, 0, new_framesize)

        data = header + framedata
        assert new_size >= len(data)
        data += (new_size - len(data)) * b'\x00'
        assert new_size == len(data)

        return data

    @convert_error(IOError, error)
    @loadfile(writable=True, create=True)
    def save(self, filething=None, v1=1, v2_version=4, v23_sep='/',
             padding=None):
        """save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None)

        Save changes to a file.

        Args:
            filething (filething):
                Filename to save the tag to. If no filename is given,
                the one most recently loaded is used.
            v1 (ID3v1SaveOptions):
                if 0, ID3v1 tags will be removed.
                if 1, ID3v1 tags will be updated but not added.
                if 2, ID3v1 tags will be created and/or updated
            v2 (int):
                version of ID3v2 tags (3 or 4).
            v23_sep (text):
                the separator used to join multiple text values
                if v2_version == 3. Defaults to '/' but if it's None
                will be the ID3v2v2.4 null separator.
            padding (:obj:`mutagen.PaddingFunction`)

        Raises:
            mutagen.MutagenError

        By default Mutagen saves ID3v2.4 tags. If you want to save ID3v2.3
        tags, you must call method update_to_v23 before saving the file.

        The lack of a way to update only an ID3v1 tag is intentional.
        """

        f = filething.fileobj

        try:
            header = ID3Header(filething.fileobj)
        except ID3NoHeaderError:
            old_size = 0
        else:
            old_size = header.size

        data = self._prepare_data(
            f, 0, old_size, v2_version, v23_sep, padding)
        new_size = len(data)

        if (old_size < new_size):
            insert_bytes(f, new_size - old_size, old_size)
        elif (old_size > new_size):
            delete_bytes(f, old_size - new_size, new_size)
        f.seek(0)
        f.write(data)

        self.__save_v1(f, v1)

    def __save_v1(self, f, v1):
        tag, offset = find_id3v1(f)
        has_v1 = tag is not None

        f.seek(offset, 2)
        if v1 == ID3v1SaveOptions.UPDATE and has_v1 or \
                v1 == ID3v1SaveOptions.CREATE:
            f.write(MakeID3v1(self))
        else:
            f.truncate()

    @loadfile(writable=True)
    def delete(self, filething=None, delete_v1=True, delete_v2=True):
        """delete(filething=None, delete_v1=True, delete_v2=True)

        Remove tags from a file.

        Args:
            filething (filething): A filename or `None` to use the one used
                when loading.
            delete_v1 (bool): delete any ID3v1 tag
            delete_v2 (bool): delete any ID3v2 tag

        If no filename is given, the one most recently loaded is used.
        """

        delete(filething, delete_v1, delete_v2)
        self.clear()
mutagen/_util.py
def delete_bytes(fobj, size, offset, BUFFER_SIZE=_DEFAULT_BUFFER_SIZE):
    """Delete size bytes of empty space starting at offset.

    fobj must be an open file object, open rb+ or
    equivalent.

    Args:
        fobj (fileobj)
        size (int): The amount of space to delete
        offset (int): The start of the space to delete
    Raises:
        IOError
    """

    if size < 0 or offset < 0:
        raise ValueError

    fobj.seek(0, 2)
    filesize = fobj.tell()
    movesize = filesize - offset - size

    if movesize < 0:
        raise ValueError

    move_bytes(fobj, offset, offset + size, movesize, BUFFER_SIZE)
    resize_file(fobj, -size, BUFFER_SIZE)
mutagen/_util.py
def convert_error(exc_src, exc_dest):
    """A decorator for reraising exceptions with a different type.
    Mostly useful for IOError.

    Args:
        exc_src (type): The source exception type
        exc_dest (type): The target exception type.
    """

    def wrap(func):

        @wraps(func)
        def wrapper(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except exc_dest:
                raise
            except exc_src as err:
                reraise(exc_dest, err, sys.exc_info()[2])

        return wrapper

    return wrap
mutagen/_util.py
def insert_bytes(fobj, size, offset, BUFFER_SIZE=_DEFAULT_BUFFER_SIZE):
    """Insert size bytes of empty space starting at offset.

    fobj must be an open file object, open rb+ or
    equivalent.

    Args:
        fobj (fileobj)
        size (int): The amount of space to insert
        offset (int): The offset at which to insert the space
    Raises:
        IOError
    """

    if size < 0 or offset < 0:
        raise ValueError

    fobj.seek(0, 2)
    filesize = fobj.tell()
    movesize = filesize - offset

    if movesize < 0:
        raise ValueError

    resize_file(fobj, size, BUFFER_SIZE)
    move_bytes(fobj, offset + size, offset, movesize, BUFFER_SIZE)
mutagen/_util.py
def reraise(tp, value, tb):
    raise tp(value).with_traceback(tb)


Question: import sys
from mutagen.id3 import ID3
from mutagen.id3._util import ID3NoHeaderError, error as ID3Error
from mutagen._util import (
    MutagenError,
    convert_error,
    delete_bytes,
    insert_bytes,
    loadfile,
    reraise,
    resize_bytes,
)
                self._fileobj.seek(next_offset)
                try:
                    chunk = self.parse_next_subchunk()
                except EmptyChunk:
                    break
                except InvalidChunk:
                    break
                self.__subchunks.append(chunk)

                # Calculate the location of the next chunk
                next_offset = chunk.offset + chunk.size
        return self.__subchunks

    def insert_chunk(self, id_, data=None):
        """Insert a new chunk at the end of the container chunk"""

        if not is_valid_chunk_id(id_):
            raise KeyError("Invalid IFF key.")

        next_offset = self.offset + self.size
        size = self.HEADER_SIZE
        data_size = 0
        if data:
            data_size = len(data)
            padding = data_size % 2
            size += data_size + padding
        insert_bytes(self._fileobj, size, next_offset)
        self._fileobj.seek(next_offset)
        self.write_new_header(id_.ljust(4).encode('ascii'), data_size)
        self._fileobj.seek(next_offset)
        chunk = self.parse_next_subchunk()
        self._update_size(chunk.size)
        if data:
            chunk.write(data)
        self.subchunks().append(chunk)
        self._fileobj.flush()
        return chunk

    def __contains__(self, id_):
        """Check if this chunk contains a specific subchunk."""
        assert_valid_chunk_id(id_)
        try:
            self[id_]
            return True
        except KeyError:
            return False

    def __getitem__(self, id_):
        """Get a subchunk by ID."""
        assert_valid_chunk_id(id_)
        found_chunk = None
        for chunk in self.subchunks():
            if chunk.id == id_:
                found_chunk = chunk
                break
        else:
            raise KeyError("No %r chunk found" % id_)
        return found_chunk

    def __delitem__(self, id_):
        """Remove a chunk from the IFF file"""
        assert_valid_chunk_id(id_)
        self[id_].delete()

    def _remove_subchunk(self, chunk):
        assert chunk in self.__subchunks
        self._update_size(-chunk.size, chunk)
        self.__subchunks.remove(chunk)

    def _update_sibling_offsets(self, changed_subchunk, size_diff):
        """Update the offsets of subchunks after `changed_subchunk`.
        """
        index = self.__subchunks.index(changed_subchunk)
        sibling_chunks = self.__subchunks[index + 1:len(self.__subchunks)]
        for sibling in sibling_chunks:
            sibling.offset -= size_diff
            sibling.data_offset -= size_diff


class IffFile:
    """Representation of a IFF file"""

    def __init__(self, chunk_cls, fileobj):
        fileobj.seek(0)
        self.root = chunk_cls.parse(fileobj)

    def __contains__(self, id_):
        """Check if the IFF file contains a specific chunk"""
        return id_ in self.root

    def __getitem__(self, id_):
        """Get a chunk from the IFF file"""
        return self.root[id_]

    def __delitem__(self, id_):
        """Remove a chunk from the IFF file"""
        self.delete_chunk(id_)

    def delete_chunk(self, id_):
        """Remove a chunk from the IFF file"""
        del self.root[id_]

    def insert_chunk(self, id_, data=None):
        """Insert a new chunk at the end of the IFF file"""
        return self.root.insert_chunk(id_, data)


class IffID3(ID3):
    """A generic IFF file with ID3v2 tags"""

    def _load_file(self, fileobj):
        raise error("Not implemented")

    def _pre_load_header(self, fileobj):
        try:
            fileobj.seek(self._load_file(fileobj)['ID3'].data_offset)
        except (InvalidChunk, KeyError):
            raise ID3NoHeaderError("No ID3 chunk")

    @convert_error(IOError, error)

Answer:     @loadfile(writable=True)