[Python-modules-commits] [mutagen] 01/01: New upstream version 1.36
Tristan Seligmann
mithrandi at moszumanska.debian.org
Mon Dec 26 12:37:47 UTC 2016
This is an automated email from the git hooks/post-receive script.
mithrandi pushed a commit to annotated tag upstream/1.36
in repository mutagen.
commit bf933649fd7be3c982295b9a0abdea64564723b8
Author: Tristan Seligmann <mithrandi at debian.org>
Date: Mon Dec 26 14:14:04 2016 +0200
New upstream version 1.36
---
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