Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge branch 'py3-for-review'

Signed-off-by: Wieland Hoffmann <themineo@gmail.com>
  • Loading branch information...
commit dc62019d0721e0a1b269dbc79f0a324c7dfd6f00 2 parents e791ea1 + 25bde22
@mineo mineo authored
View
2  musicbrainzngs/__init__.py
@@ -1 +1 @@
-from musicbrainz import *
+from .musicbrainz import *
View
62 musicbrainzngs/compat.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2012 Kenneth Reitz.
+
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""
+pythoncompat
+"""
+
+
+import sys
+
+# -------
+# Pythons
+# -------
+
+# Syntax sugar.
+_ver = sys.version_info
+
+#: Python 2.x?
+is_py2 = (_ver[0] == 2)
+
+#: Python 3.x?
+is_py3 = (_ver[0] == 3)
+
+# ---------
+# Specifics
+# ---------
+
+if is_py2:
+ from StringIO import StringIO
+ from urllib2 import HTTPPasswordMgr, HTTPDigestAuthHandler, Request,\
+ HTTPHandler, build_opener, HTTPError, URLError,\
+ build_opener
+ from httplib import BadStatusLine, HTTPException
+ from urlparse import urlunparse
+ from urllib import urlencode
+
+ bytes = str
+ unicode = unicode
+ basestring = basestring
+elif is_py3:
+ from io import StringIO
+ from urllib.request import HTTPPasswordMgr, HTTPDigestAuthHandler, Request,\
+ HTTPHandler, build_opener
+ from urllib.error import HTTPError, URLError
+ from http.client import HTTPException, BadStatusLine
+ from urllib.parse import urlunparse, urlencode
+
+ unicode = str
+ bytes = bytes
+ basestring = (str,bytes)
View
13 musicbrainzngs/mbxml.py
@@ -4,9 +4,11 @@
# See the COPYING file for more information.
import xml.etree.ElementTree as ET
-import string
-import StringIO
import logging
+
+from . import compat
+from . import util
+
try:
from ET import fixtag
except:
@@ -16,7 +18,7 @@ def fixtag(tag, namespaces):
# tag and namespace declaration, if any
if isinstance(tag, ET.QName):
tag = tag.text
- namespace_uri, tag = string.split(tag[1:], "}", 1)
+ namespace_uri, tag = tag[1:].split("}", 1)
prefix = namespaces.get(namespace_uri)
if prefix is None:
prefix = "ns%d" % len(namespaces)
@@ -29,6 +31,7 @@ def fixtag(tag, namespaces):
xmlns = None
return "%s:%s" % (prefix, tag), xmlns
+
NS_MAP = {"http://musicbrainz.org/ns/mmd-2.0#": "ws2",
"http://musicbrainz.org/ns/ext#-2.0": "ext"}
_log = logging.getLogger("python-musicbrainz-ngs")
@@ -113,9 +116,7 @@ def parse_inner(inner_els, element):
return result
def parse_message(message):
- s = message.read()
- f = StringIO.StringIO(s)
- tree = ET.ElementTree(file=f)
+ tree = util.bytes_to_elementtree(message)
root = tree.getroot()
result = {}
valid_elements = {"artist": parse_artist,
View
60 musicbrainzngs/musicbrainz.py
@@ -3,18 +3,17 @@
# This file is distributed under a BSD-2-Clause type license.
# See the COPYING file for more information.
-import urlparse
-import urllib2
-import urllib
-import mbxml
import re
import threading
import time
import logging
-import httplib
import socket
import xml.etree.ElementTree as etree
from xml.parsers import expat
+from . import mbxml
+from . import compat
+from . import util
+
_version = "0.3dev"
_log = logging.getLogger("musicbrainzngs")
@@ -187,9 +186,9 @@ def _check_filter_and_make_params(entity, includes, release_status=[], release_t
the filters can be used with the given includes. Return a params
dict that can be passed to _do_mb_query.
"""
- if isinstance(release_status, basestring):
+ if isinstance(release_status, compat.basestring):
release_status = [release_status]
- if isinstance(release_type, basestring):
+ if isinstance(release_type, compat.basestring):
release_type = [release_type]
_check_filter(release_status, VALID_RELEASE_STATUSES)
_check_filter(release_type, VALID_RELEASE_TYPES)
@@ -307,7 +306,7 @@ def __call__(self, *args, **kwargs):
# Generic support for making HTTP requests.
# From pymb2
-class _RedirectPasswordMgr(urllib2.HTTPPasswordMgr):
+class _RedirectPasswordMgr(compat.HTTPPasswordMgr):
def __init__(self):
self._realms = { }
@@ -322,18 +321,18 @@ def add_password(self, realm, uri, username, password):
# ignoring the uri parameter intentionally
self._realms[realm] = (username, password)
-class _DigestAuthHandler(urllib2.HTTPDigestAuthHandler):
+class _DigestAuthHandler(compat.HTTPDigestAuthHandler):
def get_authorization (self, req, chal):
qop = chal.get ('qop', None)
if qop and ',' in qop and 'auth' in qop.split (','):
chal['qop'] = 'auth'
- return urllib2.HTTPDigestAuthHandler.get_authorization (self, req, chal)
+ return compat.HTTPDigestAuthHandler.get_authorization (self, req, chal)
-class _MusicbrainzHttpRequest(urllib2.Request):
+class _MusicbrainzHttpRequest(compat.Request):
""" A custom request handler that allows DELETE and PUT"""
def __init__(self, method, url, data=None):
- urllib2.Request.__init__(self, url, data)
+ compat.Request.__init__(self, url, data)
allowed_m = ["GET", "POST", "DELETE", "PUT"]
if method not in allowed_m:
raise ValueError("invalid method: %s" % method)
@@ -363,7 +362,7 @@ def _safe_open(opener, req, body=None, max_retries=8, retry_delay_delta=2.0):
else:
f = opener.open(req)
- except urllib2.HTTPError, exc:
+ except compat.HTTPError as exc:
if exc.code in (400, 404, 411):
# Bad request, not found, etc.
raise ResponseError(cause=exc)
@@ -375,19 +374,19 @@ def _safe_open(opener, req, body=None, max_retries=8, retry_delay_delta=2.0):
# retrying for now.
_log.debug("unknown HTTP error %i" % exc.code)
last_exc = exc
- except httplib.BadStatusLine, exc:
+ except compat.BadStatusLine as exc:
_log.debug("bad status line")
last_exc = exc
- except httplib.HTTPException, exc:
+ except compat.HTTPException as exc:
_log.debug("miscellaneous HTTP exception: %s" % str(exc))
last_exc = exc
- except urllib2.URLError, exc:
+ except compat.URLError as exc:
if isinstance(exc.reason, socket.error):
code = exc.reason.errno
if code == 104: # "Connection reset by peer."
continue
raise NetworkError(cause=exc)
- except IOError, exc:
+ except IOError as exc:
raise NetworkError(cause=exc)
else:
# No exception! Yay!
@@ -426,23 +425,23 @@ def _mb_request(path, method='GET', auth_required=False, client_required=False,
# Encode Unicode arguments using UTF-8.
for key, value in args.items():
- if isinstance(value, unicode):
+ if isinstance(value, compat.unicode):
args[key] = value.encode('utf8')
# Construct the full URL for the request, including hostname and
# query string.
- url = urlparse.urlunparse((
+ url = compat.urlunparse((
'http',
hostname,
'/ws/2/%s' % path,
'',
- urllib.urlencode(args),
+ compat.urlencode(args),
''
))
_log.debug("%s request for %s" % (method, url))
# Set up HTTP request handler and URL opener.
- httpHandler = urllib2.HTTPHandler(debuglevel=0)
+ httpHandler = compat.HTTPHandler(debuglevel=0)
handlers = [httpHandler]
# Add credentials if required.
@@ -456,7 +455,7 @@ def _mb_request(path, method='GET', auth_required=False, client_required=False,
authHandler.add_password("musicbrainz.org", (), user, password)
handlers.append(authHandler)
- opener = urllib2.build_opener(*handlers)
+ opener = compat.build_opener(*handlers)
# Make request.
req = _MusicbrainzHttpRequest(method, url, data)
@@ -519,8 +518,11 @@ def _do_mb_search(entity, query='', fields={}, limit=None, offset=None):
for the given entity type.
"""
# Encode the query terms as a Lucene query string.
- query_parts = [query.replace('\x00', '').strip()]
- for key, value in fields.iteritems():
+ query_parts = []
+ if query:
+ clean_query = util._unicode(query)
+ query_parts.append(clean_query)
+ for key, value in fields.items():
# Ensure this is a valid search field.
if key not in VALID_SEARCH_FIELDS[entity]:
raise InvalidSearchFieldError(
@@ -528,12 +530,12 @@ def _do_mb_search(entity, query='', fields={}, limit=None, offset=None):
)
# Escape Lucene's special characters.
+ value = util._unicode(value)
value = re.sub(r'([+\-&|!(){}\[\]\^"~*?:\\])', r'\\\1', value)
- value = value.replace('\x00', '').strip()
value = value.lower() # Avoid binary operators like OR.
if value:
- query_parts.append(u'%s:(%s)' % (key, value))
- full_query = u' '.join(query_parts).strip()
+ query_parts.append('%s:(%s)' % (key, value))
+ full_query = ' '.join(query_parts).strip()
if not full_query:
raise ValueError('at least one query term is required')
@@ -769,8 +771,8 @@ def submit_ratings(artist_ratings={}, recording_ratings={}):
def add_releases_to_collection(collection, releases=[]):
# XXX: Maximum URI length of 16kb means we should only allow ~400 releases
releaselist = ";".join(releases)
- _do_mb_put("collection/%s/releases/%s" % (collection, releaselist))
+ _do_mb_put("collection/%s/releases/%s" % (collection, releaselist))
def remove_releases_from_collection(collection, releases=[]):
releaselist = ";".join(releases)
- _do_mb_delete("collection/%s/releases/%s" % (collection, releaselist))
+ _do_mb_delete("collection/%s/releases/%s" % (collection, releaselist))
View
37 musicbrainzngs/util.py
@@ -0,0 +1,37 @@
+# This file is part of the musicbrainzngs library
+# Copyright (C) Alastair Porter, Adrian Sampson, and others
+# This file is distributed under a BSD-2-Clause type license.
+# See the COPYING file for more information.
+
+import sys
+import locale
+import xml.etree.ElementTree as ET
+
+from . import compat
+
+def _unicode(string, encoding=None):
+ """Try to decode byte strings to unicode.
+ This can only be a guess, but this might be better than failing.
+ It is safe to use this on numbers or strings that are already unicode.
+ """
+ if isinstance(string, compat.unicode):
+ unicode_string = string
+ elif isinstance(string, compat.bytes):
+ # use given encoding, stdin, preferred until something != None is found
+ if encoding is None:
+ encoding = sys.stdin.encoding
+ if encoding is None:
+ encoding = locale.getpreferredencoding()
+ unicode_string = string.decode(encoding, "ignore")
+ else:
+ unicode_string = compat.unicode(string)
+ return unicode_string.replace('\x00', '').strip()
+
+def bytes_to_elementtree(_bytes):
+ if compat.is_py3:
+ s = _unicode(_bytes.read(), "utf-8")
+ else:
+ s = _bytes.read()
+ f = compat.StringIO(s)
+ tree = ET.ElementTree(file=f)
+ return tree
View
6 test/test_mbxml.py
@@ -7,8 +7,8 @@
class MbXML(unittest.TestCase):
def testMakeBarcode(self):
- expected = ('<ns0:metadata xmlns:ns0="http://musicbrainz.org/ns/mmd-2.0#">'
- '<ns0:release-list><ns0:release ns0:id="trid"><ns0:barcode>12345</ns0:barcode>'
- '</ns0:release></ns0:release-list></ns0:metadata>')
+ expected = (b'<ns0:metadata xmlns:ns0="http://musicbrainz.org/ns/mmd-2.0#">'
+ b'<ns0:release-list><ns0:release ns0:id="trid"><ns0:barcode>12345</ns0:barcode>'
+ b'</ns0:release></ns0:release-list></ns0:metadata>')
xml = mbxml.make_barcode_request({'trid':'12345'})
self.assertEqual(expected, xml)
View
34 test/test_mbxml_search.py
@@ -5,11 +5,14 @@
import musicbrainzngs
from musicbrainzngs import mbxml
-import urllib2
-import StringIO
-
-
-class FakeOpener(urllib2.OpenerDirector):
+try:
+ import StringIO
+ from urllib2 import OpenerDirector
+except ImportError:
+ import io as StringIO
+ from urllib.request import OpenerDirector
+
+class FakeOpener(OpenerDirector):
""" A URL Opener that saves the URL requested and
returns a dummy response """
def open(self, request, body=None):
@@ -19,41 +22,40 @@ def open(self, request, body=None):
def get_url(self):
return self.myurl
+opener = FakeOpener()
+
+musicbrainzngs.compat.build_opener = lambda args: opener
+
class UrlTest(unittest.TestCase):
""" Test that the correct URL is generated when a search query is made """
- def build_opener(self, *args):
- self.opener = FakeOpener()
- return self.opener
-
def setUp(self):
musicbrainzngs.set_useragent("a", "1")
musicbrainzngs.set_rate_limit(1, 100)
- urllib2.build_opener = self.build_opener
def testSearchArtist(self):
musicbrainzngs.search_artists("Dynamo Go")
- self.assertEqual("http://musicbrainz.org/ws/2/artist/?query=Dynamo+Go", self.opener.get_url())
+ self.assertEqual("http://musicbrainz.org/ws/2/artist/?query=Dynamo+Go", opener.get_url())
def testSearchWork(self):
musicbrainzngs.search_works("Fountain City")
- self.assertEqual("http://musicbrainz.org/ws/2/work/?query=Fountain+City", self.opener.get_url())
+ self.assertEqual("http://musicbrainz.org/ws/2/work/?query=Fountain+City", opener.get_url())
def testSearchLabel(self):
musicbrainzngs.search_labels("Waysafe")
- self.assertEqual("http://musicbrainz.org/ws/2/label/?query=Waysafe", self.opener.get_url())
+ self.assertEqual("http://musicbrainz.org/ws/2/label/?query=Waysafe", opener.get_url())
def testSearchRelease(self):
musicbrainzngs.search_releases("Affordable Pop Music")
- self.assertEqual("http://musicbrainz.org/ws/2/release/?query=Affordable+Pop+Music", self.opener.get_url())
+ self.assertEqual("http://musicbrainz.org/ws/2/release/?query=Affordable+Pop+Music", opener.get_url())
def testSearchReleaseGroup(self):
musicbrainzngs.search_release_groups("Affordable Pop Music")
- self.assertEqual("http://musicbrainz.org/ws/2/release-group/?query=Affordable+Pop+Music", self.opener.get_url())
+ self.assertEqual("http://musicbrainz.org/ws/2/release-group/?query=Affordable+Pop+Music", opener.get_url())
def testSearchRecording(self):
musicbrainzngs.search_recordings("Thief of Hearts")
- self.assertEqual("http://musicbrainz.org/ws/2/recording/?query=Thief+of+Hearts", self.opener.get_url())
+ self.assertEqual("http://musicbrainz.org/ws/2/recording/?query=Thief+of+Hearts", opener.get_url())
class SearchArtistTest(unittest.TestCase):
def testFields(self):
Please sign in to comment.
Something went wrong with that request. Please try again.