[Python-modules-commits] [mutagen] 01/06: Import mutagen_1.36.orig.tar.gz

Tristan Seligmann mithrandi at moszumanska.debian.org
Mon Dec 26 12:37:36 UTC 2016


This is an automated email from the git hooks/post-receive script.

mithrandi pushed a commit to branch master
in repository mutagen.

commit 5d35b4f7cd0a01ec8e6d160fa9bbf076e2dbae6d
Author: Tristan Seligmann <mithrandi at debian.org>
Date:   Mon Dec 26 14:18:08 2016 +0200

    Import mutagen_1.36.orig.tar.gz
---
 NEWS                       |  11 ++
 PKG-INFO                   |   2 +-
 docs/man/mid3cp.rst        |   4 +
 man/mid3cp.1               |   4 +
 mutagen/__init__.py        |   2 +-
 mutagen/_senf/__init__.py  |   2 +-
 mutagen/_senf/_argv.py     |  77 ++++++++++-
 mutagen/_senf/_compat.py   |   8 +-
 mutagen/_senf/_environ.py  |  31 ++++-
 mutagen/_senf/_fsnative.py | 314 ++++++++++++++++++++++++++++++++++++---------
 mutagen/_senf/_print.py    |  13 +-
 mutagen/_senf/_temp.py     |   6 +-
 mutagen/_tools/mid3cp.py   |  61 ++++++---
 mutagen/_tools/mid3v2.py   |  13 +-
 mutagen/easyid3.py         |  30 ++++-
 mutagen/flac.py            |   7 +
 mutagen/id3/_specs.py      |  10 +-
 mutagen/id3/_tags.py       |  44 +++++--
 mutagen/mp4/__init__.py    |  20 ++-
 tests/test__id3frames.py   |  13 ++
 tests/test__id3specs.py    |  10 +-
 tests/test_easyid3.py      |  46 ++++++-
 tests/test_flac.py         |  11 ++
 tests/test_id3.py          |  10 ++
 tests/test_mp4.py          |  24 +++-
 tests/test_tools_mid3cp.py |  35 +++++
 26 files changed, 662 insertions(+), 146 deletions(-)

diff --git a/NEWS b/NEWS
index 0d629b8..6e2589c 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,14 @@
+1.36 - 2016.12.22
+-----------------
+
+* ID3: Ignore trailing empty values for v2.3 text frames :bug:`276`
+* ID3: Write large APIC frames last :bug:`278`
+* EasyID3: support saving as v2.3 :bug:`188`
+* FLAC: Add StreamInfo.bitrate :bug:`279`
+* mid3cp: Add ``--merge`` option :bug:`277`
+* MP4: Allow loading files without audio tracks :bug:`272`
+
+
 1.35.1 - 2016.11.09
 -------------------
 
diff --git a/PKG-INFO b/PKG-INFO
index 96d39af..0d2e275 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: mutagen
-Version: 1.35.1
+Version: 1.36
 Summary: read and write audio tags for many formats
 Home-page: https://github.com/quodlibet/mutagen
 Author: Michael Urman
diff --git a/docs/man/mid3cp.rst b/docs/man/mid3cp.rst
index bd86f52..f1c6a39 100644
--- a/docs/man/mid3cp.rst
+++ b/docs/man/mid3cp.rst
@@ -37,6 +37,10 @@ OPTIONS
 --exclude-tag, -x
     Exclude a specific tag from being copied. Can be specified multiple times.
 
+--merge
+    Copy over frames instead of replacing the whole ID3 tag. The tag version
+    of *dest* will be used. In case *dest* has no ID3 tag this option has no
+    effect.
 
 
 AUTHOR
diff --git a/man/mid3cp.1 b/man/mid3cp.1
index 9e7de8e..cf559b5 100644
--- a/man/mid3cp.1
+++ b/man/mid3cp.1
@@ -51,6 +51,10 @@ Write ID3v1 tags to the destination file, derived from the ID3v2 tags.
 .TP
 .B \-\-exclude\-tag\fP,\fB  \-x
 Exclude a specific tag from being copied. Can be specified multiple times.
+.TP
+.B \-\-merge
+Copy over frames instead of replacing the whole ID3 tag. The tag version
+of \fIdest\fP will be used.
 .UNINDENT
 .SH AUTHOR
 .sp
diff --git a/mutagen/__init__.py b/mutagen/__init__.py
index c996b77..b9c0088 100644
--- a/mutagen/__init__.py
+++ b/mutagen/__init__.py
@@ -24,7 +24,7 @@ from mutagen._util import MutagenError
 from mutagen._file import FileType, StreamInfo, File
 from mutagen._tags import Tags, Metadata, PaddingInfo
 
-version = (1, 35, 1)
+version = (1, 36)
 """Version tuple."""
 
 version_string = ".".join(map(str, version))
diff --git a/mutagen/_senf/__init__.py b/mutagen/_senf/__init__.py
index f2fcb4f..38b0aff 100644
--- a/mutagen/_senf/__init__.py
+++ b/mutagen/_senf/__init__.py
@@ -33,7 +33,7 @@ fsnative, print_, getcwd, getenv, unsetenv, putenv, environ, expandvars, \
     gettempdir, gettempprefix, mkdtemp, input_, expanduser, text2fsn
 
 
-version = (1, 0, 1)
+version = (1, 2, 2)
 """Tuple[`int`, `int`, `int`]: The version tuple (major, minor, micro)"""
 
 
diff --git a/mutagen/_senf/_argv.py b/mutagen/_senf/_argv.py
index 14df5c6..56b1d41 100644
--- a/mutagen/_senf/_argv.py
+++ b/mutagen/_senf/_argv.py
@@ -14,17 +14,22 @@
 
 import sys
 import ctypes
+import collections
+from functools import total_ordering
 
-from ._compat import PY2
-from ._fsnative import is_unix
+from ._compat import PY2, string_types
+from ._fsnative import is_win, _fsn2legacy, path2fsn
 from . import _winapi as winapi
 
 
-def create_argv():
-    """Returns a unicode argv under Windows and standard sys.argv otherwise"""
+def _get_win_argv():
+    """Returns a unicode argv under Windows and standard sys.argv otherwise
 
-    if is_unix or not PY2:
-        return sys.argv
+    Returns:
+        List[`fsnative`]
+    """
+
+    assert is_win
 
     argc = ctypes.c_int()
     try:
@@ -43,4 +48,62 @@ def create_argv():
     return res
 
 
-argv = create_argv()
+ at total_ordering
+class Argv(collections.MutableSequence):
+    """List[`fsnative`]: Like `sys.argv` but contains unicode
+    keys and values under Windows + Python 2.
+
+    Any changes made will be forwarded to `sys.argv`.
+    """
+
+    def __init__(self):
+        if PY2 and is_win:
+            self._argv = _get_win_argv()
+        else:
+            self._argv = sys.argv
+
+    def __getitem__(self, index):
+        return self._argv[index]
+
+    def __setitem__(self, index, value):
+        if isinstance(value, string_types):
+            value = path2fsn(value)
+
+        self._argv[index] = value
+
+        if sys.argv is not self._argv:
+            try:
+                if isinstance(value, string_types):
+                    sys.argv[index] = _fsn2legacy(value)
+                else:
+                    sys.argv[index] = [_fsn2legacy(path2fsn(v)) for v in value]
+            except IndexError:
+                pass
+
+    def __delitem__(self, index):
+        del self._argv[index]
+        try:
+            del sys.argv[index]
+        except IndexError:
+            pass
+
+    def __eq__(self, other):
+        return self._argv == other
+
+    def __lt__(self, other):
+        return self._argv < other
+
+    def __len__(self):
+        return len(self._argv)
+
+    def __repr__(self):
+        return repr(self._argv)
+
+    def insert(self, index, value):
+        value = path2fsn(value)
+        self._argv.insert(index, value)
+        if sys.argv is not self._argv:
+            sys.argv.insert(index, _fsn2legacy(value))
+
+
+argv = Argv()
diff --git a/mutagen/_senf/_compat.py b/mutagen/_senf/_compat.py
index 6a1c9cc..a31c4b8 100644
--- a/mutagen/_senf/_compat.py
+++ b/mutagen/_senf/_compat.py
@@ -20,8 +20,8 @@ PY3 = not PY2
 
 
 if PY2:
-    from urlparse import urlparse
-    urlparse
+    from urlparse import urlparse, urlunparse
+    urlparse, urlunparse
     from urllib import pathname2url, url2pathname, quote, unquote
     pathname2url, url2pathname, quote, unquote
 
@@ -35,8 +35,8 @@ if PY2:
 
     iteritems = lambda d: d.iteritems()
 elif PY3:
-    from urllib.parse import urlparse, quote, unquote
-    urlparse, quote, unquote
+    from urllib.parse import urlparse, quote, unquote, urlunparse
+    urlparse, quote, unquote, urlunparse
     from urllib.request import pathname2url, url2pathname
     pathname2url, url2pathname
 
diff --git a/mutagen/_senf/_environ.py b/mutagen/_senf/_environ.py
index 5903783..4f8a064 100644
--- a/mutagen/_senf/_environ.py
+++ b/mutagen/_senf/_environ.py
@@ -17,7 +17,7 @@ import ctypes
 import collections
 
 from ._compat import text_type, PY2
-from ._fsnative import path2fsn, is_win
+from ._fsnative import path2fsn, is_win, _fsn2legacy, fsnative
 from . import _winapi as winapi
 
 
@@ -105,6 +105,7 @@ def read_windows_environ():
             key, value = entry.split(u"=", 1)
         except ValueError:
             continue
+        key = _norm_key(key)
         dict_[key] = value
 
     status = winapi.FreeEnvironmentStringsW(res)
@@ -114,9 +115,18 @@ def read_windows_environ():
     return dict_
 
 
+def _norm_key(key):
+    assert isinstance(key, fsnative)
+    if is_win:
+        key = key.upper()
+    return key
+
+
 class Environ(collections.MutableMapping):
     """Dict[`fsnative`, `fsnative`]: Like `os.environ` but contains unicode
-    keys and values under Windows + Python 2
+    keys and values under Windows + Python 2.
+
+    Any changes made will be forwarded to `os.environ`.
     """
 
     def __init__(self):
@@ -130,14 +140,20 @@ class Environ(collections.MutableMapping):
         self._env = env
 
     def __getitem__(self, key):
-        key = path2fsn(key)
+        key = _norm_key(path2fsn(key))
         return self._env[key]
 
     def __setitem__(self, key, value):
-        key = path2fsn(key)
+        key = _norm_key(path2fsn(key))
         value = path2fsn(value)
 
         if is_win and PY2:
+            # this calls putenv, so do it first and replace later
+            try:
+                os.environ[_fsn2legacy(key)] = _fsn2legacy(value)
+            except OSError:
+                raise ValueError
+
             try:
                 set_windows_env_var(key, value)
             except WindowsError:
@@ -149,7 +165,7 @@ class Environ(collections.MutableMapping):
             raise ValueError
 
     def __delitem__(self, key):
-        key = path2fsn(key)
+        key = _norm_key(path2fsn(key))
 
         if is_win and PY2:
             try:
@@ -157,6 +173,11 @@ class Environ(collections.MutableMapping):
             except WindowsError:
                 pass
 
+            try:
+                del os.environ[_fsn2legacy(key)]
+            except KeyError:
+                pass
+
         del self._env[key]
 
     def __iter__(self):
diff --git a/mutagen/_senf/_fsnative.py b/mutagen/_senf/_fsnative.py
index 7b62bc6..c210995 100644
--- a/mutagen/_senf/_fsnative.py
+++ b/mutagen/_senf/_fsnative.py
@@ -19,7 +19,7 @@ import codecs
 
 from . import _winapi as winapi
 from ._compat import text_type, PY3, PY2, url2pathname, urlparse, quote, \
-    unquote
+    unquote, urlunparse
 
 
 is_win = os.name == "nt"
@@ -29,8 +29,64 @@ is_darwin = sys.platform == "darwin"
 _surrogatepass = "strict" if PY2 else "surrogatepass"
 
 
-def _decode_surrogatepass(data, codec):
-    """Like data.decode(codec, 'surrogatepass') but makes utf-16-le work
+def _normalize_codec(codec, _cache={}):
+    """Raises LookupError"""
+
+    try:
+        return _cache[codec]
+    except KeyError:
+        _cache[codec] = codecs.lookup(codec).name
+        return _cache[codec]
+
+
+def _swap_bytes(data):
+    """swaps bytes for 16 bit, leaves remaining trailing bytes alone"""
+
+    a, b = data[1::2], data[::2]
+    data = bytearray().join(bytearray(x) for x in zip(a, b))
+    if len(b) > len(a):
+        data += b[-1:]
+    return bytes(data)
+
+
+def _codec_fails_on_encode_surrogates(codec, _cache={}):
+    """Returns if a codec fails correctly when passing in surrogates with
+    a surrogatepass/surrogateescape error handler. Some codecs were broken
+    in Python <3.4
+    """
+
+    try:
+        return _cache[codec]
+    except KeyError:
+        try:
+            u"\uD800\uDC01".encode(codec)
+        except UnicodeEncodeError:
+            _cache[codec] = True
+        else:
+            _cache[codec] = False
+        return _cache[codec]
+
+
+def _codec_can_decode_with_surrogatepass(codec, _cache={}):
+    """Returns if a codec supports the surrogatepass error handler when
+    decoding. Some codecs were broken in Python <3.4
+    """
+
+    try:
+        return _cache[codec]
+    except KeyError:
+        try:
+            u"\ud83d".encode(
+                codec, _surrogatepass).decode(codec, _surrogatepass)
+        except UnicodeDecodeError:
+            _cache[codec] = False
+        else:
+            _cache[codec] = True
+        return _cache[codec]
+
+
+def _bytes2winpath(data, codec):
+    """Like data.decode(codec, 'surrogatepass') but makes utf-16-le/be work
     on Python < 3.4 + Windows
 
     https://bugs.python.org/issue27971
@@ -41,17 +97,64 @@ def _decode_surrogatepass(data, codec):
     try:
         return data.decode(codec, _surrogatepass)
     except UnicodeDecodeError:
-        if os.name == "nt" and sys.version_info[:2] < (3, 4) and \
-                codecs.lookup(codec).name == "utf-16-le":
-            buffer_ = ctypes.create_string_buffer(data + b"\x00\x00")
-            value = ctypes.wstring_at(buffer_, len(data) // 2)
-            if value.encode("utf-16-le", _surrogatepass) != data:
+        if not _codec_can_decode_with_surrogatepass(codec):
+            if _normalize_codec(codec) == "utf-16-be":
+                data = _swap_bytes(data)
+                codec = "utf-16-le"
+            if _normalize_codec(codec) == "utf-16-le":
+                buffer_ = ctypes.create_string_buffer(data + b"\x00\x00")
+                value = ctypes.wstring_at(buffer_, len(data) // 2)
+                if value.encode("utf-16-le", _surrogatepass) != data:
+                    raise
+                return value
+            else:
                 raise
-            return value
         else:
             raise
 
 
+def _winpath2bytes_py3(text, codec):
+    """Fallback implementation for text including surrogates"""
+
+    # merge surrogate codepoints
+    if _normalize_codec(codec).startswith("utf-16"):
+        # fast path, utf-16 merges anyway
+        return text.encode(codec, _surrogatepass)
+    return _bytes2winpath(
+        text.encode("utf-16-le", _surrogatepass),
+        "utf-16-le").encode(codec, _surrogatepass)
+
+
+if PY2:
+    def _winpath2bytes(text, codec):
+        return text.encode(codec)
+else:
+    def _winpath2bytes(text, codec):
+        if _codec_fails_on_encode_surrogates(codec):
+            try:
+                return text.encode(codec)
+            except UnicodeEncodeError:
+                return _winpath2bytes_py3(text, codec)
+        else:
+            return _winpath2bytes_py3(text, codec)
+
+
+def _fsn2legacy(path):
+    """Takes a fsnative path and returns a path that can be put into os.environ
+    or sys.argv. Might result in a mangled path on Python2 + Windows.
+    Can't fail.
+
+    Args:
+        path (fsnative)
+    Returns:
+        str
+    """
+
+    if PY2 and is_win:
+        return path.encode(_encoding, "replace")
+    return path
+
+
 def _fsnative(text):
     if not isinstance(text, text_type):
         raise TypeError("%r needs to be a text type (%r)" % (text, text_type))
@@ -68,10 +171,16 @@ def _fsnative(text):
             path = text.encode(encoding, _surrogatepass)
         except UnicodeEncodeError:
             path = text.encode("utf-8", _surrogatepass)
+
+        if b"\x00" in path:
+            path = path.replace(b"\x00", fsn2bytes(_fsnative(u"\uFFFD"), None))
+
         if PY3:
             return path.decode(_encoding, "surrogateescape")
         return path
     else:
+        if u"\x00" in text:
+            text = text.replace(u"\x00", u"\uFFFD")
         return text
 
 
@@ -82,13 +191,7 @@ def _create_fsnative(type_):
     class meta(type):
 
         def __instancecheck__(self, instance):
-            # XXX: invalid str on Unix + Py3 still returns True here, but
-            # might fail when passed to fsnative API. We could be more strict
-            # here and call _validate_fsnative(), but then we could
-            # have a value not being an instance of fsnative, while its type
-            # is still a subclass of fsnative.. and this is enough magic
-            # already.
-            return isinstance(instance, type_)
+            return _typecheck_fsnative(instance)
 
         def __subclasscheck__(self, subclass):
             return issubclass(subclass, type_)
@@ -115,13 +218,21 @@ def _create_fsnative(type_):
 
         The real returned type is:
 
-        - Python 2 + Windows: :obj:`python:unicode` with ``surrogates``
-        - Python 2 + Unix: :obj:`python:str`
-        - Python 3 + Windows: :obj:`python3:str` with ``surrogates``
-        - Python 3 + Unix: :obj:`python3:str` with ``surrogates`` (only
-          containing code points which can be encoded with the locale encoding)
+        - **Python 2 + Windows:** :obj:`python:unicode`, with ``surrogates``,
+          without ``null``
+        - **Python 2 + Unix:** :obj:`python:str`, without ``null``
+        - **Python 3 + Windows:** :obj:`python3:str`, with ``surrogates``,
+          without ``null``
+        - **Python 3 + Unix:** :obj:`python3:str`, with ``surrogates``, without
+          ``null``, without code points not encodable with the locale encoding
 
         Constructing a `fsnative` can't fail.
+
+        Passing a `fsnative` to :func:`open` will never lead to `ValueError`
+        or `TypeError`.
+
+        Any operation on `fsnative` can also use the `str` type, as long as
+        the `str` only contains ASCII and no NULL.
         """
 
         def __new__(cls, text=u""):
@@ -136,7 +247,33 @@ fsnative_type = text_type if is_win or PY3 else bytes
 fsnative = _create_fsnative(fsnative_type)
 
 
-def _validate_fsnative(path):
+def _typecheck_fsnative(path):
+    """
+    Args:
+        path (object)
+    Returns:
+        bool: if path is a fsnative
+    """
+
+    if not isinstance(path, fsnative_type):
+        return False
+
+    if PY3 or is_win:
+        if u"\x00" in path:
+            return False
+
+        if is_unix and not _is_unicode_encoding:
+            try:
+                path.encode(_encoding, "surrogateescape")
+            except UnicodeEncodeError:
+                return False
+    elif b"\x00" in path:
+        return False
+
+    return True
+
+
+def _fsn2native(path):
     """
     Args:
         path (fsnative)
@@ -155,16 +292,25 @@ def _validate_fsnative(path):
         raise TypeError("path needs to be %s, not %s" % (
             fsnative_type.__name__, type(path).__name__))
 
-    if PY3 and is_unix:
-        try:
-            return path.encode(_encoding, "surrogateescape")
-        except UnicodeEncodeError:
-            # This look more like ValueError, but raising only one error
-            # makes things simpler... also one could say str + surrogates
-            # is its own type
-            raise TypeError("path contained Unicode code points not valid in"
-                            "the current path encoding. To create a valid "
-                            "path from Unicode use text2fsn()")
+    if is_unix:
+        if PY3:
+            try:
+                path = path.encode(_encoding, "surrogateescape")
+            except UnicodeEncodeError:
+                assert not _is_unicode_encoding
+                # This look more like ValueError, but raising only one error
+                # makes things simpler... also one could say str + surrogates
+                # is its own type
+                raise TypeError(
+                    "path contained Unicode code points not valid in"
+                    "the current path encoding. To create a valid "
+                    "path from Unicode use text2fsn()")
+
+        if b"\x00" in path:
+            raise TypeError("fsnative can't contain nulls")
+    else:
+        if u"\x00" in path:
+            raise TypeError("fsnative can't contain nulls")
 
     return path
 
@@ -175,14 +321,17 @@ def _get_encoding():
     encoding = sys.getfilesystemencoding()
     if encoding is None:
         if is_darwin:
-            return "utf-8"
+            encoding = "utf-8"
         elif is_win:
-            return "mbcs"
+            encoding = "mbcs"
         else:
-            return "ascii"
+            encoding = "ascii"
+    encoding = _normalize_codec(encoding)
     return encoding
 
+
 _encoding = _get_encoding()
+_is_unicode_encoding = _encoding.startswith("utf")
 
 
 def path2fsn(path):
@@ -201,19 +350,28 @@ def path2fsn(path):
     # allow mbcs str on py2+win and bytes on py3
     if PY2:
         if is_win:
-            if isinstance(path, str):
+            if isinstance(path, bytes):
                 path = path.decode(_encoding)
         else:
-            if isinstance(path, unicode):
+            if isinstance(path, text_type):
                 path = path.encode(_encoding)
+        if "\x00" in path:
+            raise ValueError("embedded null")
     else:
         path = getattr(os, "fspath", lambda x: x)(path)
         if isinstance(path, bytes):
+            if b"\x00" in path:
+                raise ValueError("embedded null")
             path = path.decode(_encoding, "surrogateescape")
         elif is_unix and isinstance(path, str):
             # make sure we can encode it and this is not just some random
             # unicode string
-            path.encode(_encoding, "surrogateescape")
+            data = path.encode(_encoding, "surrogateescape")
+            if b"\x00" in data:
+                raise ValueError("embedded null")
+        else:
+            if u"\x00" in path:
+                raise ValueError("embedded null")
 
     if not isinstance(path, fsnative_type):
         raise TypeError("path needs to be %s", fsnative_type.__name__)
@@ -221,29 +379,38 @@ def path2fsn(path):
     return path
 
 
-def fsn2text(path):
+def fsn2text(path, strict=False):
     """
     Args:
         path (fsnative): The path to convert
+        strict (bool): Fail in case the conversion is not reversible
     Returns:
         `text`
     Raises:
         TypeError: In case no `fsnative` has been passed
+        ValueError: In case ``strict`` was True and the conversion failed
 
     Converts a `fsnative` path to `text`.
 
-    This process is not reversible and should only be used for display
-    purposes.
+    Can be used to pass a path to some unicode API, like for example a GUI
+    toolkit.
+
+    If ``strict`` is True the conversion will fail in case it is not
+    reversible. This can be useful for converting program arguments that are
+    supposed to be text and erroring out in case they are not.
+
     Encoding with a Unicode encoding will always succeed with the result.
     """
 
-    path = _validate_fsnative(path)
+    path = _fsn2native(path)
+
+    errors = "strict" if strict else "replace"
 
     if is_win:
         return path.encode("utf-16-le", _surrogatepass).decode("utf-16-le",
-                                                               "replace")
+                                                               errors)
     else:
-        return path.decode(_encoding, "replace")
+        return path.decode(_encoding, errors)
 
 
 def text2fsn(text):
@@ -274,21 +441,26 @@ def fsn2bytes(path, encoding):
         TypeError: If no `fsnative` path is passed
         ValueError: If encoding fails or no encoding is given
 
-    Turns a `fsnative` path to `bytes`.
+    Converts a `fsnative` path to `bytes`.
 
     The passed *encoding* is only used on platforms where paths are not
     associated with an encoding (Windows for example). If you don't care about
     Windows you can pass `None`.
+
+    For Windows paths, lone surrogates will be encoded like normal code points
+    and surrogate pairs will be merged before encoding. In case of ``utf-8``
+    or ``utf-16-le`` this is equal to the `WTF-8 and WTF-16 encoding
+    <https://simonsapin.github.io/wtf-8/>`__.
     """
 
-    path = _validate_fsnative(path)
+    path = _fsn2native(path)
 
     if is_win:
         if encoding is None:
             raise ValueError("invalid encoding %r" % encoding)
 
         try:
-            return path.encode(encoding, _surrogatepass)
+            return _winpath2bytes(path, encoding)
         except LookupError:
             raise ValueError("invalid encoding %r" % encoding)
     else:
@@ -320,13 +492,19 @@ def bytes2fsn(data, encoding):
         if encoding is None:
             raise ValueError("invalid encoding %r" % encoding)
         try:
-            return _decode_surrogatepass(data, encoding)
+            path = _bytes2winpath(data, encoding)
         except LookupError:
             raise ValueError("invalid encoding %r" % encoding)
-    elif PY2:
-        return data
+        if u"\x00" in path:
+            raise ValueError("contains nulls")
+        return path
     else:
-        return data.decode(_encoding, "surrogateescape")
+        if b"\x00" in data:
+            raise ValueError("contains nulls")
+        if PY2:
+            return data
+        else:
+            return data.decode(_encoding, "surrogateescape")
 
 
 def uri2fsn(uri):
@@ -343,7 +521,7 @@ def uri2fsn(uri):
     """
 
     if PY2:
-        if isinstance(uri, unicode):
+        if isinstance(uri, text_type):
             uri = uri.encode("utf-8")
         if not isinstance(uri, bytes):
             raise TypeError("uri needs to be ascii str or unicode")
@@ -357,20 +535,29 @@ def uri2fsn(uri):
     path = parsed.path
 
     if scheme != "file":
-        raise ValueError("Not a file URI")
+        raise ValueError("Not a file URI: %r" % uri)
+
+    if not path:
+        raise ValueError("Invalid file URI: %r" % uri)
+
+    uri = urlunparse(parsed)[7:]
 
     if is_win:
-        path = url2pathname(netloc + path)
+        path = url2pathname(uri)
         if netloc:
             path = "\\\\" + path
         if PY2:
             path = path.decode("utf-8")
+        if u"\x00" in path:
+            raise ValueError("embedded null")
         return path
     else:
-        if PY2:
-            return url2pathname(path)
-        else:
-            return fsnative(url2pathname(path))
+        path = url2pathname(uri)
+        if "\x00" in path:
+            raise ValueError("embedded null")
+        if PY3:
+            path = fsnative(path)
+        return path
 
 
 def fsn2uri(path):
@@ -378,7 +565,7 @@ def fsn2uri(path):
     Args:
         path (fsnative): The path to convert to an URI
     Returns:
-        `str`: An ASCII only URI
+        `text`: An ASCII only URI
     Raises:
         TypeError: If no `fsnative` was passed
         ValueError: If the path can't be converted
@@ -389,11 +576,14 @@ def fsn2uri(path):
     percent encoded.
     """
 
-    path = _validate_fsnative(path)
+    path = _fsn2native(path)
 
     def _quote_path(path):
         # RFC 2396
-        return quote(path, "/:@&=+$,")
+        path = quote(path, "/:@&=+$,")
+        if PY2:
+            path = path.decode("ascii")
+        return path
 
     if is_win:
         buf = ctypes.create_unicode_buffer(winapi.INTERNET_MAX_URL_LENGTH)
@@ -417,4 +607,4 @@ def fsn2uri(path):
         return _quote_path(uri.encode("utf-8", _surrogatepass))
 
     else:
-        return "file://" + _quote_path(path)
+        return u"file://" + _quote_path(path)
diff --git a/mutagen/_senf/_print.py b/mutagen/_senf/_print.py
index 18946cb..051118e 100644
--- a/mutagen/_senf/_print.py
+++ b/mutagen/_senf/_print.py
@@ -33,8 +33,7 @@ def print_(*objects, **kwargs):
         file (object): A file-like object, defaults to `sys.stdout`
         flush (bool): If the file stream should be flushed
     Raises:
-        OSError
-        IOError
+        EnvironmentError
 
     Like print(), but:
 
@@ -128,7 +127,7 @@ def _print_windows(objects, sep, end, file, flush):
 
     try:
         fileno = file.fileno()
-    except (IOError, AttributeError):
+    except (EnvironmentError, AttributeError):
         pass
     else:
         if fileno == 1:
@@ -202,13 +201,16 @@ def _print_windows(objects, sep, end, file, flush):
         except (TypeError, ValueError):
             file.write(text)
 
+        if flush:
+            file.flush()
+
 
 def _readline_windows():
     """Raises OSError"""
 
     try:
         fileno = sys.stdin.fileno()
-    except (IOError, AttributeError):
+    except (EnvironmentError, AttributeError):
         fileno = -1
 
     # In case stdin is replaced, read from that
@@ -337,8 +339,7 @@ def input_(prompt=None):
     Returns:
         `fsnative`
     Raises:
-        OSError
-        IOError
+        EnvironmentError
 
     Like :func:`python3:input` but returns a `fsnative` and allows printing
     filenames as prompt to stdout.
diff --git a/mutagen/_senf/_temp.py b/mutagen/_senf/_temp.py
index a194076..ac44dfb 100644
--- a/mutagen/_senf/_temp.py
+++ b/mutagen/_senf/_temp.py
@@ -54,8 +54,7 @@ def mkstemp(suffix=None, prefix=None, dir=None, text=False):
         Tuple[`int`, `fsnative`]:
             A tuple containing the file descriptor and the file path
     Raises:
-        OSError
-        IOError
+        EnvironmentError
 
     Like :func:`python3:tempfile.mkstemp` but always returns a `fsnative`
     path.
@@ -77,8 +76,7 @@ def mkdtemp(suffix=None, prefix=None, dir=None):
     Returns:
         `fsnative`: A path to a directory
     Raises:
-        OSError
-        IOError
+        EnvironmentError
 
     Like :func:`python3:tempfile.mkstemp` but always returns a `fsnative` path.
     """
diff --git a/mutagen/_tools/mid3cp.py b/mutagen/_tools/mid3cp.py
index 99eaba3..7d96f63 100644
--- a/mutagen/_tools/mid3cp.py
+++ b/mutagen/_tools/mid3cp.py
@@ -42,7 +42,7 @@ class ID3OptionParser(OptionParser):
                          "replacement for id3lib's id3cp."))
 
 
-def copy(src, dst, write_v1=True, excluded_tags=None, verbose=False):
+def copy(src, dst, merge, write_v1=True, excluded_tags=None, verbose=False):
     """Returns 0 on success"""
 
     if excluded_tags is None:
@@ -56,32 +56,47 @@ def copy(src, dst, write_v1=True, excluded_tags=None, verbose=False):
     except Exception as err:
         print_(str(err), file=sys.stderr)
         return 1
-    else:
-        if verbose:
-            print_(u"File", src, u"contains:", file=sys.stderr)
-            print_(id3.pprint(), file=sys.stderr)
 
-        for tag in excluded_tags:
-            id3.delall(tag)
+    if verbose:
+        print_(u"File", src, u"contains:", file=sys.stderr)
+        print_(id3.pprint(), file=sys.stderr)
 
-        # if the source is 2.3 save it as 2.3
-        if id3.version < (2, 4, 0):
-            id3.update_to_v23()
-            v2_version = 3
-        else:
-            id3.update_to_v24()
-            v2_version = 4
+    for tag in excluded_tags:
+        id3.delall(tag)
 
+    if merge:
         try:
-            id3.save(dst, v1=(2 if write_v1 else 0), v2_version=v2_version)
+            target = mutagen.id3.ID3(dst, translate=False)
+        except mutagen.id3.ID3NoHeaderError:
+            # no need to merge
+            pass
         except Exception as err:
-            print_(u"Error saving", dst, u":\n%s" % text_type(err),
-                   file=sys.stderr)
+            print_(str(err), file=sys.stderr)
             return 1
         else:
-            if verbose:
-                print_(u"Successfully saved", dst, file=sys.stderr)
-            return 0
+            for frame in id3.values():
+                target.add(frame)
+
+            id3 = target
+
+    # if the source is 2.3 save it as 2.3
+    if id3.version < (2, 4, 0):
+        id3.update_to_v23()
+        v2_version = 3
+    else:
+        id3.update_to_v24()
+        v2_version = 4
+
+    try:
+        id3.save(dst, v1=(2 if write_v1 else 0), v2_version=v2_version)
+    except Exception as err:
+        print_(u"Error saving", dst, u":\n%s" % text_type(err),
+               file=sys.stderr)
+        return 1
+    else:
+        if verbose:
+            print_(u"Successfully saved", dst, file=sys.stderr)
+        return 0
 
 
 def main(argv):
@@ -92,6 +107,9 @@ def main(argv):
                       default=False, help="write id3v1 tags")
     parser.add_option("-x", "--exclude-tag", metavar="TAG", action="append",
                       dest="x", help="exclude the specified tag", default=[])
+    parser.add_option("--merge", action="store_true",
+                      help="Copy over frames instead of the whole ID3 tag",
... 561 lines suppressed ...

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/mutagen.git



More information about the Python-modules-commits mailing list