예제 #1
0
    def test_iri_support(self):
        self.assertEqual(
            urls.uri_to_iri('http://xn--n3h.net/'),
            'http://\u2603.net/'
        )
        self.assertEqual(
            urls.uri_to_iri(
                'http://%C3%BCser:p%C3%[email protected]/p%C3%A5th'
            ),
            'http://\xfcser:p\xe4ssword@\u2603.net/p\xe5th'
        )
        self.assertEqual(
            urls.iri_to_uri('http://☃.net/'),
            'http://xn--n3h.net/'
        )
        self.assertEqual(
            urls.iri_to_uri('http://üser:pässword@☃.net/påth'),
            'http://%C3%BCser:p%C3%[email protected]/p%C3%A5th'
        )

        self.assertEqual(
            urls.uri_to_iri('http://test.com/%3Fmeh?foo=%26%2F'),
            'http://test.com/%3Fmeh?foo=%26%2F'
        )

        self.assertEqual(urls.iri_to_uri('/foo'), '/foo')

        self.assertEqual(
            urls.iri_to_uri('http://föö.com:8080/bam/baz'),
            'http://xn--f-1gaa.com:8080/bam/baz'
        )
예제 #2
0
    def test_iri_support(self):
        self.assertEqual(
            urls.uri_to_iri('http://xn--n3h.net/'),
            'http://\u2603.net/'
        )
        self.assertEqual(
            urls.uri_to_iri(
                'http://%C3%BCser:p%C3%[email protected]/p%C3%A5th'
            ),
            'http://\xfcser:p\xe4ssword@\u2603.net/p\xe5th'
        )
        self.assertEqual(
            urls.iri_to_uri('http://☃.net/'),
            'http://xn--n3h.net/'
        )
        self.assertEqual(
            urls.iri_to_uri('http://üser:pässword@☃.net/påth'),
            'http://%C3%BCser:p%C3%[email protected]/p%C3%A5th'
        )

        self.assertEqual(
            urls.uri_to_iri('http://test.com/%3Fmeh?foo=%26%2F'),
            'http://test.com/%3Fmeh?foo=%26%2F'
        )

        self.assertEqual(urls.iri_to_uri('/foo'), '/foo')

        self.assertEqual(
            urls.iri_to_uri('http://föö.com:8080/bam/baz'),
            'http://xn--f-1gaa.com:8080/bam/baz'
        )
예제 #3
0
 def test_iri_safe_conversion(self):
     self.assertEqual(
         urls.iri_to_uri('magnet:?foo=bar'),
         'magnet:?foo=bar'
     )
     self.assertEqual(
         urls.iri_to_uri('itms-service://?foo=bar'),
         'itms-service:?foo=bar'
     )
     self.assertEqual(
         urls.iri_to_uri('itms-service://?foo=bar', safe_conversion=True),
         'itms-service://?foo=bar'
     )
예제 #4
0
def redirect(location, code=302, Response=None):
    """Returns a response object (a WSGI application) that, if called,
    redirects the client to the target location.  Supported codes are 301,
    302, 303, 305, and 307.  300 is not supported because it's not a real
    redirect and 304 because it's the answer for a request with a request
    with defined If-Modified-Since headers.

    :param location:
        The location the response should redirect to.
    :param code:
        The redirect status code. defaults to 302.
    :param class Response:
        A Response class to use when instantiating a response. The default is
        :class:`verktyg.responses.Response` if unspecified.
    """
    if Response is None:
        from verktyg.responses import Response

    display_location = escape(location)
    if isinstance(location, str):
        # Safe conversion is necessary here as we might redirect
        # to a broken URI scheme (for instance itms-services).
        from verktyg.urls import iri_to_uri
        location = iri_to_uri(location, safe_conversion=True)
    response = Response(
        '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\n'
        '<title>Redirecting...</title>\n'
        '<h1>Redirecting...</h1>\n'
        '<p>You should be redirected automatically to target URL: '
        '<a href="%s">%s</a>.  If not click the link.' %
        (escape(location), display_location),
        code,
        mimetype='text/html')
    response.headers['Location'] = location
    return response
예제 #5
0
파일: utils.py 프로젝트: bwhmather/verktyg
def redirect(location, code=302, Response=None):
    """Returns a response object (a WSGI application) that, if called,
    redirects the client to the target location.  Supported codes are 301,
    302, 303, 305, and 307.  300 is not supported because it's not a real
    redirect and 304 because it's the answer for a request with a request
    with defined If-Modified-Since headers.

    :param location:
        The location the response should redirect to.
    :param code:
        The redirect status code. defaults to 302.
    :param class Response:
        A Response class to use when instantiating a response. The default is
        :class:`verktyg.responses.Response` if unspecified.
    """
    if Response is None:
        from verktyg.responses import Response

    display_location = escape(location)
    if isinstance(location, str):
        # Safe conversion is necessary here as we might redirect
        # to a broken URI scheme (for instance itms-services).
        from verktyg.urls import iri_to_uri
        location = iri_to_uri(location)
    response = Response(
        '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\n'
        '<title>Redirecting...</title>\n'
        '<h1>Redirecting...</h1>\n'
        '<p>You should be redirected automatically to target URL: '
        '<a href="%s">%s</a>.  If not click the link.' %
        (escape(location), display_location),
        code, mimetype='text/html'
    )
    response.headers['Location'] = location
    return response
예제 #6
0
파일: test.py 프로젝트: bwhmather/verktyg
    def __init__(
        self, path='/', base_url=None, query_string=None,
        method='GET', input_stream=None, content_type=None,
        content_length=None, errors_stream=None, multithread=False,
        multiprocess=False, run_once=False, headers=None, data=None,
        environ_base=None, environ_overrides=None, charset='utf-8',
    ):
        if query_string is None and '?' in path:
            path, query_string = path.split('?', 1)
        self.charset = charset
        self.path = iri_to_uri(path)
        if base_url is not None:
            base_url = url_fix(base_url)
        self.base_url = base_url
        if isinstance(query_string, (bytes, str)):
            self.query_string = query_string
        else:
            if query_string is None:
                query_string = MultiDict()
            elif not isinstance(query_string, MultiDict):
                query_string = MultiDict(query_string)
            self.args = query_string
        self.method = method
        if headers is None:
            headers = Headers()
        elif not isinstance(headers, Headers):
            headers = Headers(headers)
        self.headers = headers
        if content_type is not None:
            self.content_type = content_type
        if errors_stream is None:
            errors_stream = sys.stderr
        self.errors_stream = errors_stream
        self.multithread = multithread
        self.multiprocess = multiprocess
        self.run_once = run_once
        self.environ_base = environ_base
        self.environ_overrides = environ_overrides
        self.input_stream = input_stream
        self.content_length = content_length
        self.closed = False

        if data:
            if input_stream is not None:
                raise TypeError('can\'t provide input stream and data')
            if isinstance(data, str):
                data = data.encode(self.charset)
            if isinstance(data, bytes):
                self.input_stream = BytesIO(data)
                if self.content_length is None:
                    self.content_length = len(data)
            else:
                for key, value in _iter_data(data):
                    if (
                        isinstance(value, (tuple, dict)) or
                        hasattr(value, 'read')
                    ):
                        self._add_file_from_data(key, value)
                    else:
                        self.form.setlistdefault(key).append(value)
예제 #7
0
    def test_uri_iri_normalization(self):
        uri = 'http://xn--f-rgao.com/%E2%98%90/fred?utf8=%E2%9C%93'
        iri = 'http://föñ.com/\N{BALLOT BOX}/fred?utf8=\u2713'

        tests = [
            'http://föñ.com/\N{BALLOT BOX}/fred?utf8=\u2713',
            'http://xn--f-rgao.com/\u2610/fred?utf8=\N{CHECK MARK}',
            'http://xn--f-rgao.com/%E2%98%90/fred?utf8=%E2%9C%93',
            'http://xn--f-rgao.com/%E2%98%90/fred?utf8=%E2%9C%93',
            'http://föñ.com/\u2610/fred?utf8=%E2%9C%93',
        ]

        for test in tests:
            self.assertEqual(urls.uri_to_iri(test), iri)
            self.assertEqual(urls.iri_to_uri(test), uri)
            self.assertEqual(urls.uri_to_iri(urls.iri_to_uri(test)), iri)
            self.assertEqual(urls.iri_to_uri(urls.uri_to_iri(test)), uri)
            self.assertEqual(urls.uri_to_iri(urls.uri_to_iri(test)), iri)
            self.assertEqual(urls.iri_to_uri(urls.iri_to_uri(test)), uri)
예제 #8
0
    def test_uri_iri_normalization(self):
        uri = 'http://xn--f-rgao.com/%E2%98%90/fred?utf8=%E2%9C%93'
        iri = 'http://föñ.com/\N{BALLOT BOX}/fred?utf8=\u2713'

        tests = [
            'http://föñ.com/\N{BALLOT BOX}/fred?utf8=\u2713',
            'http://xn--f-rgao.com/\u2610/fred?utf8=\N{CHECK MARK}',
            'http://xn--f-rgao.com/%E2%98%90/fred?utf8=%E2%9C%93',
            'http://xn--f-rgao.com/%E2%98%90/fred?utf8=%E2%9C%93',
            'http://föñ.com/\u2610/fred?utf8=%E2%9C%93',
        ]

        for test in tests:
            self.assertEqual(urls.uri_to_iri(test), iri)
            self.assertEqual(urls.iri_to_uri(test), uri)
            self.assertEqual(urls.uri_to_iri(urls.iri_to_uri(test)), iri)
            self.assertEqual(urls.iri_to_uri(urls.uri_to_iri(test)), uri)
            self.assertEqual(urls.uri_to_iri(urls.uri_to_iri(test)), iri)
            self.assertEqual(urls.iri_to_uri(urls.iri_to_uri(test)), uri)
예제 #9
0
    def __init__(self,
                 path='/',
                 base_url=None,
                 query_string=None,
                 method='GET',
                 input_stream=None,
                 content_type=None,
                 content_length=None,
                 errors_stream=None,
                 multithread=False,
                 multiprocess=False,
                 run_once=False,
                 headers=None,
                 data=None,
                 environ_base=None,
                 environ_overrides=None,
                 charset='utf-8'):
        if query_string is None and '?' in path:
            path, query_string = path.split('?', 1)
        self.charset = charset
        self.path = iri_to_uri(path)
        if base_url is not None:
            base_url = url_fix(iri_to_uri(base_url, charset), charset)
        self.base_url = base_url
        if isinstance(query_string, (bytes, str)):
            self.query_string = query_string
        else:
            if query_string is None:
                query_string = MultiDict()
            elif not isinstance(query_string, MultiDict):
                query_string = MultiDict(query_string)
            self.args = query_string
        self.method = method
        if headers is None:
            headers = Headers()
        elif not isinstance(headers, Headers):
            headers = Headers(headers)
        self.headers = headers
        if content_type is not None:
            self.content_type = content_type
        if errors_stream is None:
            errors_stream = sys.stderr
        self.errors_stream = errors_stream
        self.multithread = multithread
        self.multiprocess = multiprocess
        self.run_once = run_once
        self.environ_base = environ_base
        self.environ_overrides = environ_overrides
        self.input_stream = input_stream
        self.content_length = content_length
        self.closed = False

        if data:
            if input_stream is not None:
                raise TypeError('can\'t provide input stream and data')
            if isinstance(data, str):
                data = data.encode(self.charset)
            if isinstance(data, bytes):
                self.input_stream = BytesIO(data)
                if self.content_length is None:
                    self.content_length = len(data)
            else:
                for key, value in _iter_data(data):
                    if (isinstance(value, (tuple, dict))
                            or hasattr(value, 'read')):
                        self._add_file_from_data(key, value)
                    else:
                        self.form.setlistdefault(key).append(value)
예제 #10
0
 def test_iri_safe_quoting(self):
     uri = 'http://xn--f-1gaa.com/%2F%25?q=%C3%B6&x=%3D%25#%25'
     iri = 'http://föö.com/%2F%25?q=ö&x=%3D%25#%25'
     self.assertEqual(urls.uri_to_iri(uri), iri)
     self.assertEqual(urls.iri_to_uri(urls.uri_to_iri(uri)), uri)
예제 #11
0
 def test_uri_to_iri_to_uri(self):
     uri = 'http://xn--f-rgao.com/%C3%9E'
     iri = urls.uri_to_iri(uri)
     self.assertEqual(urls.iri_to_uri(iri), uri)
예제 #12
0
 def test_iri_to_uri_to_iri(self):
     iri = 'http://föö.com/'
     uri = urls.iri_to_uri(iri)
     self.assertEqual(urls.uri_to_iri(uri), iri)
예제 #13
0
 def test_iri_to_uri_idempotence_non_ascii(self):
     uri = 'http://\N{SNOWMAN}/\N{SNOWMAN}'
     uri = urls.iri_to_uri(uri)
     self.assertEqual(urls.iri_to_uri(uri), uri)
예제 #14
0
 def test_iri_to_uri_to_iri(self):
     iri = 'http://föö.com/'
     uri = urls.iri_to_uri(iri)
     self.assertEqual(urls.uri_to_iri(uri), iri)
예제 #15
0
def dump_cookie(
    key, value='', max_age=None, expires=None, path='/',
    domain=None, secure=False, httponly=False,
    charset='utf-8', sync_expires=True,
):
    """Creates a new Set-Cookie header without the ``Set-Cookie`` prefix
    The parameters are the same as in the cookie Morsel object in the
    Python standard library but it accepts unicode data, too.

    The return value of this function will be a unicode string tunneled through
    latin1 as required by PEP 3333.

    The return value is not ASCII safe if the key contains unicode
    characters.  This is technically against the specification but
    happens in the wild.  It's strongly recommended to not use
    non-ASCII values for the keys.

    :param max_age:
        Should be a number of seconds, or `None` (default) if the cookie should
        last only as long as the client's browser session.  Additionally
        `timedelta` objects are accepted, too.
    :param expires:
        Should be a `datetime` object or unix timestamp.
    :param path:
        Limits the cookie to a given path, per default it will span the whole
        domain.
    :param domain:
        Use this if you want to set a cross-domain cookie. For example,
        ``domain=".example.com"`` will set a cookie that is readable by the
        domain ``www.example.com``, ``foo.example.com`` etc. Otherwise, a
        cookie will only be readable by the domain that set it.
    :param secure:
        The cookie will only be available via HTTPS.
    :param httponly:
        Disallow JavaScript to access the cookie.  This is an extension to the
        cookie standard and probably not supported by all browsers.
    :param charset:
        The encoding for unicode values.
    :param sync_expires:
        Automatically set expires if max_age is defined but expires not.
    """
    key = key.encode(charset)
    value = value.encode(charset)

    if path is not None:
        path = iri_to_uri(path)
    domain = _make_cookie_domain(domain)
    if isinstance(max_age, timedelta):
        max_age = (max_age.days * 60 * 60 * 24) + max_age.seconds
    if expires is not None:
        if not isinstance(expires, str):
            expires = cookie_date(expires)
    elif max_age is not None and sync_expires:
        expires = cookie_date(time() + max_age).encode('ascii')

    buf = [key + b'=' + _cookie_quote(value)]

    # XXX: In theory all of these parameters that are not marked with `None`
    # should be quoted.  Because stdlib did not quote it before I did not
    # want to introduce quoting there now.
    for k, v, q in (
        (b'Domain', domain, True),
        (b'Expires', expires, False,),
        (b'Max-Age', max_age, False),
        (b'Secure', secure, None),
        (b'HttpOnly', httponly, None),
        (b'Path', path, False),
    ):
        if q is None:
            if v:
                buf.append(k)
            continue

        if v is None:
            continue

        tmp = bytearray(k)
        if not isinstance(v, (bytes, bytearray)):
            v = str(v).encode(charset)
        if q:
            v = _cookie_quote(v)
        tmp += b'=' + v
        buf.append(bytes(tmp))

    # The return value will be an incorrectly encoded latin1 header on
    # for consistency with the headers object
    rv = b'; '.join(buf)
    rv = rv.decode('latin1')
    return rv
예제 #16
0
    def get_wsgi_headers(self, environ):
        """This is automatically called right before the response is started
        and returns headers modified for the given environment.  It returns a
        copy of the headers from the response with some modifications applied
        if necessary.

        For example the location header (if present) is joined with the root
        URL of the environment.  Also the content length is automatically set
        to zero here for certain status codes.

        :param environ:
            The WSGI environment of the request.
        :return:
            Returns a new :class:`~verktyg.http.Headers` object.
        """
        headers = Headers(self.headers)
        location = None
        content_location = None
        content_length = None
        status = self.status_code

        # iterate over the headers to find all values in one go.  Because
        # get_wsgi_headers is used each response that gives us a tiny
        # speedup.
        for key, value in headers:
            ikey = key.lower()
            if ikey == u'location':
                location = value
            elif ikey == u'content-location':
                content_location = value
            elif ikey == u'content-length':
                content_length = value

        # make sure the location header is an absolute URL
        if location is not None:
            old_location = location
            if isinstance(location, str):
                # Safe conversion is necessary here as we might redirect
                # to a broken URI scheme (for instance itms-services).
                location = iri_to_uri(location)

            if self.autocorrect_location_header:
                current_url = get_current_url(environ, root_only=True)
                if isinstance(current_url, str):
                    current_url = iri_to_uri(current_url)
                location = urljoin(current_url, location)
            if location != old_location:
                headers['Location'] = location

        # make sure the content location is a URL
        if (
            content_location is not None and
            isinstance(content_location, str)
        ):
            headers['Content-Location'] = iri_to_uri(content_location)

        # remove entity headers and set content length to zero if needed.
        # Also update content_length accordingly so that the automatic
        # content length detection does not trigger in the following
        # code.
        if 100 <= status < 200 or status == 204:
            headers['Content-Length'] = content_length = u'0'
        elif status == 304:
            remove_entity_headers(headers)

        # if we can determine the content length automatically, we
        # should try to do that.  But only if this does not involve
        # flattening the iterator or encoding of unicode strings in
        # the response.  We however should not do that if we have a 304
        # response.
        if (
            self.automatically_set_content_length and
            self.is_sequence and content_length is None and status != 304
        ):
            content_length = sum(len(x) for x in self.response)
            headers['Content-Length'] = str(content_length)

        return headers
예제 #17
0
 def test_iri_to_uri_idempotence_ascii_only(self):
     uri = 'http://www.idempoten.ce'
     uri = urls.iri_to_uri(uri)
     self.assertEqual(urls.iri_to_uri(uri), uri)
예제 #18
0
 def test_uri_to_iri_to_uri(self):
     uri = 'http://xn--f-rgao.com/%C3%9E'
     iri = urls.uri_to_iri(uri)
     self.assertEqual(urls.iri_to_uri(iri), uri)
예제 #19
0
def dump_cookie(key,
                value='',
                max_age=None,
                expires=None,
                path='/',
                domain=None,
                secure=False,
                httponly=False,
                charset='utf-8',
                sync_expires=True):
    """Creates a new Set-Cookie header without the ``Set-Cookie`` prefix
    The parameters are the same as in the cookie Morsel object in the
    Python standard library but it accepts unicode data, too.

    The return value of this function will be a unicode string tunneled through
    latin1 as required by PEP 3333.

    The return value is not ASCII safe if the key contains unicode
    characters.  This is technically against the specification but
    happens in the wild.  It's strongly recommended to not use
    non-ASCII values for the keys.

    :param max_age:
        Should be a number of seconds, or `None` (default) if the cookie should
        last only as long as the client's browser session.  Additionally
        `timedelta` objects are accepted, too.
    :param expires:
        Should be a `datetime` object or unix timestamp.
    :param path:
        Limits the cookie to a given path, per default it will span the whole
        domain.
    :param domain:
        Use this if you want to set a cross-domain cookie. For example,
        ``domain=".example.com"`` will set a cookie that is readable by the
        domain ``www.example.com``, ``foo.example.com`` etc. Otherwise, a
        cookie will only be readable by the domain that set it.
    :param secure:
        The cookie will only be available via HTTPS.
    :param httponly:
        Disallow JavaScript to access the cookie.  This is an extension to the
        cookie standard and probably not supported by all browsers.
    :param charset:
        The encoding for unicode values.
    :param sync_expires:
        Automatically set expires if max_age is defined but expires not.
    """
    key = key.encode(charset)
    value = value.encode(charset)

    if path is not None:
        path = iri_to_uri(path, charset)
    domain = _make_cookie_domain(domain)
    if isinstance(max_age, timedelta):
        max_age = (max_age.days * 60 * 60 * 24) + max_age.seconds
    if expires is not None:
        if not isinstance(expires, str):
            expires = cookie_date(expires)
    elif max_age is not None and sync_expires:
        expires = cookie_date(time() + max_age).encode('ascii')

    buf = [key + b'=' + _cookie_quote(value)]

    # XXX: In theory all of these parameters that are not marked with `None`
    # should be quoted.  Because stdlib did not quote it before I did not
    # want to introduce quoting there now.
    for k, v, q in ((b'Domain', domain, True), (
            b'Expires',
            expires,
            False,
    ), (b'Max-Age', max_age, False), (b'Secure', secure, None),
                    (b'HttpOnly', httponly, None), (b'Path', path, False)):
        if q is None:
            if v:
                buf.append(k)
            continue

        if v is None:
            continue

        tmp = bytearray(k)
        if not isinstance(v, (bytes, bytearray)):
            v = str(v).encode(charset)
        if q:
            v = _cookie_quote(v)
        tmp += b'=' + v
        buf.append(bytes(tmp))

    # The return value will be an incorrectly encoded latin1 header on
    # for consistency with the headers object
    rv = b'; '.join(buf)
    rv = rv.decode('latin1')
    return rv
예제 #20
0
 def test_iri_to_uri_idempotence_ascii_only(self):
     uri = 'http://www.idempoten.ce'
     uri = urls.iri_to_uri(uri)
     self.assertEqual(urls.iri_to_uri(uri), uri)
예제 #21
0
 def test_iri_to_uri_idempotence_non_ascii(self):
     uri = 'http://\N{SNOWMAN}/\N{SNOWMAN}'
     uri = urls.iri_to_uri(uri)
     self.assertEqual(urls.iri_to_uri(uri), uri)
예제 #22
0
    def get_wsgi_headers(self, environ):
        """This is automatically called right before the response is started
        and returns headers modified for the given environment.  It returns a
        copy of the headers from the response with some modifications applied
        if necessary.

        For example the location header (if present) is joined with the root
        URL of the environment.  Also the content length is automatically set
        to zero here for certain status codes.

        :param environ: the WSGI environment of the request.
        :return: returns a new :class:`~verktyg.http.Headers`
                 object.
        """
        headers = Headers(self.headers)
        location = None
        content_location = None
        content_length = None
        status = self.status_code

        # iterate over the headers to find all values in one go.  Because
        # get_wsgi_headers is used each response that gives us a tiny
        # speedup.
        for key, value in headers:
            ikey = key.lower()
            if ikey == u'location':
                location = value
            elif ikey == u'content-location':
                content_location = value
            elif ikey == u'content-length':
                content_length = value

        # make sure the location header is an absolute URL
        if location is not None:
            old_location = location
            if isinstance(location, str):
                # Safe conversion is necessary here as we might redirect
                # to a broken URI scheme (for instance itms-services).
                location = iri_to_uri(location, safe_conversion=True)

            if self.autocorrect_location_header:
                current_url = get_current_url(environ, root_only=True)
                if isinstance(current_url, str):
                    current_url = iri_to_uri(current_url)
                location = urljoin(current_url, location)
            if location != old_location:
                headers['Location'] = location

        # make sure the content location is a URL
        if (
            content_location is not None and
            isinstance(content_location, str)
        ):
            headers['Content-Location'] = iri_to_uri(content_location)

        # remove entity headers and set content length to zero if needed.
        # Also update content_length accordingly so that the automatic
        # content length detection does not trigger in the following
        # code.
        if 100 <= status < 200 or status == 204:
            headers['Content-Length'] = content_length = u'0'
        elif status == 304:
            remove_entity_headers(headers)

        # if we can determine the content length automatically, we
        # should try to do that.  But only if this does not involve
        # flattening the iterator or encoding of unicode strings in
        # the response.  We however should not do that if we have a 304
        # response.
        if (
            self.automatically_set_content_length and
            self.is_sequence and content_length is None and status != 304
        ):
            try:
                content_length = sum(len(to_bytes(x, 'ascii'))
                                     for x in self.response)
            except UnicodeError:
                # aha, something non-bytestringy in there, too bad, we
                # can't safely figure out the length of the response.
                pass
            else:
                headers['Content-Length'] = str(content_length)

        return headers
예제 #23
0
 def test_iri_safe_quoting(self):
     uri = 'http://xn--f-1gaa.com/%2F%25?q=%C3%B6&x=%3D%25#%25'
     iri = 'http://föö.com/%2F%25?q=ö&x=%3D%25#%25'
     self.assertEqual(urls.uri_to_iri(uri), iri)
     self.assertEqual(urls.iri_to_uri(urls.uri_to_iri(uri)), uri)