added updated version of oauthlib
authorBenjamin Mako Hill <mako@atdot.cc>
Fri, 23 Oct 2015 02:44:08 +0000 (19:44 -0700)
committerBenjamin Mako Hill <mako@atdot.cc>
Fri, 23 Oct 2015 02:44:08 +0000 (19:44 -0700)
20 files changed:
oauthlib/__init__.py
oauthlib/common.py
oauthlib/oauth1/rfc5849/__init__.py
oauthlib/oauth1/rfc5849/endpoints/access_token.py
oauthlib/oauth1/rfc5849/endpoints/base.py
oauthlib/oauth1/rfc5849/endpoints/request_token.py
oauthlib/oauth1/rfc5849/endpoints/resource.py
oauthlib/oauth1/rfc5849/endpoints/signature_only.py
oauthlib/oauth1/rfc5849/request_validator.py
oauthlib/oauth1/rfc5849/signature.py
oauthlib/oauth2/rfc6749/clients/base.py
oauthlib/oauth2/rfc6749/endpoints/revocation.py
oauthlib/oauth2/rfc6749/errors.py
oauthlib/oauth2/rfc6749/grant_types/authorization_code.py
oauthlib/oauth2/rfc6749/grant_types/base.py
oauthlib/oauth2/rfc6749/grant_types/client_credentials.py
oauthlib/oauth2/rfc6749/grant_types/implicit.py
oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py
oauthlib/oauth2/rfc6749/request_validator.py
oauthlib/oauth2/rfc6749/utils.py

index 300bdc7..50afc05 100644 (file)
@@ -10,7 +10,7 @@
 """
 
 __author__ = 'Idan Gazit <idan@gazit.me>'
-__version__ = '0.7.2'
+__version__ = '1.0.3'
 
 
 import logging
index 0179b8e..ed2b699 100644 (file)
@@ -36,6 +36,8 @@ UNICODE_ASCII_CHARACTER_SET = ('abcdefghijklmnopqrstuvwxyz'
 CLIENT_ID_CHARACTER_SET = (r' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMN'
                            'OPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}')
 
+PASSWORD_PATTERN = re.compile(r'password=[^&]+')
+INVALID_HEX_PATTERN = re.compile(r'%[^0-9A-Fa-f]|%[0-9A-Fa-f][^0-9A-Fa-f]')
 
 always_safe = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
                'abcdefghijklmnopqrstuvwxyz'
@@ -107,7 +109,7 @@ def decode_params_utf8(params):
     return decoded
 
 
-urlencoded = set(always_safe) | set('=&;%+~,*@')
+urlencoded = set(always_safe) | set('=&;%+~,*@!')
 
 
 def urldecode(query):
@@ -132,8 +134,7 @@ def urldecode(query):
     # All encoded values begin with % followed by two hex characters
     # correct = %00, %A0, %0A, %FF
     # invalid = %G0, %5H, %PO
-    invalid_hex = '%[^0-9A-Fa-f]|%[0-9A-Fa-f][^0-9A-Fa-f]'
-    if len(re.findall(invalid_hex, query)):
+    if INVALID_HEX_PATTERN.search(query):
         raise ValueError('Invalid hex encoding in query string.')
 
     # We encode to utf-8 prior to parsing because parse_qsl behaves
@@ -378,20 +379,44 @@ class Request(object):
         self.http_method = encode(http_method)
         self.headers = CaseInsensitiveDict(encode(headers or {}))
         self.body = encode(body)
-        self.decoded_body = extract_params(encode(body))
+        self.decoded_body = extract_params(self.body)
         self.oauth_params = []
-
-        self._params = {}
+        self.validator_log = {}
+
+        self._params = {
+            "access_token": None,
+            "client": None,
+            "client_id": None,
+            "client_secret": None,
+            "code": None,
+            "extra_credentials": None,
+            "grant_type": None,
+            "redirect_uri": None,
+            "refresh_token": None,
+            "response_type": None,
+            "scope": None,
+            "scopes": None,
+            "state": None,
+            "token": None,
+            "user": None,
+            "token_type_hint": None,
+        }
         self._params.update(dict(urldecode(self.uri_query)))
         self._params.update(dict(self.decoded_body or []))
         self._params.update(self.headers)
 
     def __getattr__(self, name):
-        return self._params.get(name, None)
+        if name in self._params:
+            return self._params[name]
+        else:
+            raise AttributeError(name)
 
     def __repr__(self):
+        body = self.body
+        if body and 'password=' in body:
+            body = PASSWORD_PATTERN.sub('password=***', body)
         return '<oauthlib.Request url="%s", http_method="%s", headers="%s", body="%s">' % (
-            self.uri, self.http_method, self.headers, self.body)
+            self.uri, self.http_method, self.headers, body)
 
     @property
     def uri_query(self):
index ad9713c..56b8c6f 100644 (file)
@@ -7,7 +7,8 @@ This module is an implementation of various logic needed
 for signing and checking OAuth 1.0 RFC 5849 requests.
 """
 from __future__ import absolute_import, unicode_literals
-
+import base64
+import hashlib
 import logging
 log = logging.getLogger(__name__)
 
@@ -101,10 +102,6 @@ class Client(object):
         self.nonce = encode(nonce)
         self.timestamp = encode(timestamp)
 
-        if self.signature_method == SIGNATURE_RSA and self.rsa_key is None:
-            raise ValueError(
-                'rsa_key is required when using RSA signature method.')
-
     def __repr__(self):
         attrs = vars(self).copy()
         attrs['client_secret'] = '****' if attrs['client_secret'] else None
@@ -176,6 +173,16 @@ class Client(object):
         if self.verifier:
             params.append(('oauth_verifier', self.verifier))
 
+        # providing body hash for requests other than x-www-form-urlencoded
+        # as described in http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html
+        # 4.1.1. When to include the body hash
+        #    *  [...] MUST NOT include an oauth_body_hash parameter on requests with form-encoded request bodies
+        #    *  [...] SHOULD include the oauth_body_hash parameter on all other requests.
+        content_type = request.headers.get('Content-Type', None)
+        content_type_eligible = content_type and content_type.find('application/x-www-form-urlencoded') < 0
+        if request.body is not None and content_type_eligible:
+            params.append(('oauth_body_hash', base64.b64encode(hashlib.sha1(request.body.encode('utf-8')).digest()).decode('utf-8')))
+
         return params
 
     def _render(self, request, formencode=False, realm=None):
index 26db919..567965a 100644 (file)
@@ -192,6 +192,13 @@ class AccessTokenEndpoint(BaseEndpoint):
 
         valid_signature = self._check_signature(request, is_token_request=True)
 
+        # log the results to the validator_log
+        # this lets us handle internal reporting and analysis
+        request.validator_log['client'] = valid_client
+        request.validator_log['resource_owner'] = valid_resource_owner
+        request.validator_log['verifier'] = valid_verifier
+        request.validator_log['signature'] = valid_signature
+
         # We delay checking validity until the very end, using dummy values for
         # calculations and fetching secrets/keys to ensure the flow of every
         # request remains almost identical regardless of whether valid values
index 42006a1..42d9363 100644 (file)
@@ -84,7 +84,7 @@ class BaseEndpoint(object):
         # receiving a request with duplicated protocol parameters.
         if len(dict(oauth_params)) != len(oauth_params):
             raise errors.InvalidRequestError(
-                description='Duplicate OAuth2 entries.')
+                description='Duplicate OAuth1 entries.')
 
         oauth_params = dict(oauth_params)
         request.signature = oauth_params.get('oauth_signature')
index e97c34b..4a76abc 100644 (file)
@@ -187,6 +187,13 @@ class RequestTokenEndpoint(BaseEndpoint):
 
         valid_signature = self._check_signature(request)
 
+        # log the results to the validator_log
+        # this lets us handle internal reporting and analysis
+        request.validator_log['client'] = valid_client
+        request.validator_log['realm'] = valid_realm
+        request.validator_log['callback'] = valid_redirect
+        request.validator_log['signature'] = valid_signature
+
         # We delay checking validity until the very end, using dummy values for
         # calculations and fetching secrets/keys to ensure the flow of every
         # request remains almost identical regardless of whether valid values
index 651a87c..6021627 100644 (file)
@@ -142,6 +142,13 @@ class ResourceEndpoint(BaseEndpoint):
 
         valid_signature = self._check_signature(request)
 
+        # log the results to the validator_log
+        # this lets us handle internal reporting and analysis
+        request.validator_log['client'] = valid_client
+        request.validator_log['resource_owner'] = valid_resource_owner
+        request.validator_log['realm'] = valid_realm
+        request.validator_log['signature'] = valid_signature
+
         # We delay checking validity until the very end, using dummy values for
         # calculations and fetching secrets/keys to ensure the flow of every
         # request remains almost identical regardless of whether valid values
index 2f8e7c9..d609b7c 100644 (file)
@@ -61,6 +61,11 @@ class SignatureOnlyEndpoint(BaseEndpoint):
 
         valid_signature = self._check_signature(request)
 
+        # log the results to the validator_log
+        # this lets us handle internal reporting and analysis
+        request.validator_log['client'] = valid_client
+        request.validator_log['signature'] = valid_signature
+
         # We delay checking validity until the very end, using dummy values for
         # calculations and fetching secrets/keys to ensure the flow of every
         # request remains almost identical regardless of whether valid values
index e722029..30a3694 100644 (file)
@@ -428,7 +428,7 @@ class RequestValidator(object):
         :param client_key: The client/consumer key.
         :param request_token: The request token string.
         :param request: An oauthlib.common.Request object.
-        :returns: The rsa public key as a string.
+        :returns: None
 
         Per `Section 2.3`__ of the spec:
 
index f57d80a..8fa22ba 100644 (file)
@@ -500,6 +500,8 @@ def sign_rsa_sha1(base_string, rsa_private_key):
 
 
 def sign_rsa_sha1_with_client(base_string, client):
+    if not client.rsa_key:
+        raise ValueError('rsa_key is required when using RSA signature method.')
     return sign_rsa_sha1(base_string, client.rsa_key)
 
 
index e53ccc1..4af9e76 100644 (file)
@@ -229,7 +229,7 @@ class Client(object):
         self.redirect_url = redirect_url or self.redirect_url
         self.scope = scope or self.scope
         auth_url = self.prepare_request_uri(
-                authorization_url, redirect_uri=self.redirect_uri,
+                authorization_url, redirect_uri=self.redirect_url,
                 scope=self.scope, state=self.state, **kwargs)
         return auth_url, FORM_ENC_HEADERS, ''
 
@@ -297,7 +297,7 @@ class Client(object):
             raise InsecureTransportError()
 
         self.scope = scope or self.scope
-        body = self._client.prepare_refresh_body(body=body,
+        body = self.prepare_refresh_body(body=body,
                 refresh_token=refresh_token, scope=self.scope, **kwargs)
         return token_url, FORM_ENC_HEADERS, body
 
index b73131c..662c793 100644 (file)
@@ -74,7 +74,7 @@ class RevocationEndpoint(BaseEndpoint):
         self.request_validator.revoke_token(request.token,
                                             request.token_type_hint, request)
 
-        response_body = None
+        response_body = ''
         if self.enable_jsonp and request.callback:
             response_body = request.callback + '();'
         return {}, response_body, 200
@@ -120,8 +120,9 @@ class RevocationEndpoint(BaseEndpoint):
             raise InvalidRequestError(request=request,
                                       description='Missing token parameter.')
 
-        if not self.request_validator.authenticate_client(request):
-            raise InvalidClientError(request=request)
+        if self.request_validator.client_authentication_required(request):
+            if not self.request_validator.authenticate_client(request):
+                raise InvalidClientError(request=request)
 
         if (request.token_type_hint and
                 request.token_type_hint in self.valid_token_types and
index a21d0bd..88f5375 100644 (file)
@@ -3,7 +3,7 @@
 oauthlib.oauth2.rfc6749.errors
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Error used both by OAuth 2 clients and provicers to represent the spec
+Error used both by OAuth 2 clients and providers to represent the spec
 defined error responses for all four core grant types.
 """
 from __future__ import unicode_literals
@@ -122,24 +122,32 @@ class FatalClientError(OAuth2Error):
     pass
 
 
-class InvalidRedirectURIError(FatalClientError):
-    error = 'invalid_redirect_uri'
+class InvalidRequestFatalError(FatalClientError):
+    """For fatal errors, the request is missing a required parameter, includes
+    an invalid parameter value, includes a parameter more than once, or is
+    otherwise malformed.
+    """
+    error = 'invalid_request'
+
 
+class InvalidRedirectURIError(InvalidRequestFatalError):
+    description = 'Invalid redirect URI.'
 
-class MissingRedirectURIError(FatalClientError):
-    error = 'missing_redirect_uri'
 
+class MissingRedirectURIError(InvalidRequestFatalError):
+    description = 'Missing redirect URI.'
 
-class MismatchingRedirectURIError(FatalClientError):
-    error = 'mismatching_redirect_uri'
 
+class MismatchingRedirectURIError(InvalidRequestFatalError):
+    description = 'Mismatching redirect URI.'
 
-class MissingClientIdError(FatalClientError):
-    error = 'invalid_client_id'
 
+class InvalidClientIdError(InvalidRequestFatalError):
+    description = 'Invalid client_id parameter value.'
 
-class InvalidClientIdError(FatalClientError):
-    error = 'invalid_client_id'
+
+class MissingClientIdError(InvalidRequestFatalError):
+    description = 'Missing client_id parameter.'
 
 
 class InvalidRequestError(OAuth2Error):
@@ -151,6 +159,10 @@ class InvalidRequestError(OAuth2Error):
     error = 'invalid_request'
 
 
+class MissingResponseTypeError(InvalidRequestError):
+    description = 'Missing response_type parameter.'
+
+
 class AccessDeniedError(OAuth2Error):
 
     """The resource owner or authorization server denied the request."""
index b6ff07c..658f5ad 100644 (file)
@@ -264,6 +264,15 @@ class AuthorizationCodeGrant(GrantTypeBase):
         # error and MUST NOT automatically redirect the user-agent to the
         # invalid redirection URI.
 
+        # First check duplicate parameters
+        for param in ('client_id', 'response_type', 'redirect_uri', 'scope', 'state'):
+            try:
+                duplicate_params = request.duplicate_params
+            except ValueError:
+                raise errors.InvalidRequestFatalError(description='Unable to parse query string', request=request)
+            if param in duplicate_params:
+                raise errors.InvalidRequestFatalError(description='Duplicate %s parameter.' % param, request=request)
+
         # REQUIRED. The client identifier as described in Section 2.2.
         # http://tools.ietf.org/html/rfc6749#section-2.2
         if not request.client_id:
@@ -304,23 +313,22 @@ class AuthorizationCodeGrant(GrantTypeBase):
 
         # Note that the correct parameters to be added are automatically
         # populated through the use of specific exceptions.
-        if request.response_type is None:
-            raise errors.InvalidRequestError(description='Missing response_type parameter.', request=request)
 
-        for param in ('client_id', 'response_type', 'redirect_uri', 'scope', 'state'):
-            if param in request.duplicate_params:
-                raise errors.InvalidRequestError(description='Duplicate %s parameter.' % param, request=request)
+        # REQUIRED.
+        if request.response_type is None:
+            raise errors.MissingResponseTypeError(request=request)
+        # Value MUST be set to "code".
+        elif request.response_type != 'code':
+            raise errors.UnsupportedResponseTypeError(request=request)
 
         if not self.request_validator.validate_response_type(request.client_id,
-                                                             request.response_type, request.client, request):
+                                                             request.response_type,
+                                                             request.client, request):
+
             log.debug('Client %s is not authorized to use response_type %s.',
                       request.client_id, request.response_type)
             raise errors.UnauthorizedClientError(request=request)
 
-        # REQUIRED. Value MUST be set to "code".
-        if request.response_type != 'code':
-            raise errors.UnsupportedResponseTypeError(request=request)
-
         # OPTIONAL. The scope of the access request as described by Section 3.3
         # http://tools.ietf.org/html/rfc6749#section-3.3
         self.validate_scopes(request)
@@ -379,8 +387,8 @@ class AuthorizationCodeGrant(GrantTypeBase):
                       request.client_id, request.client, request.scopes)
             raise errors.InvalidGrantError(request=request)
 
-        for attr in ('user', 'state', 'scopes'):
-            if getattr(request, attr) is None:
+        for attr in ('user', 'scopes'):
+            if getattr(request, attr, None) is None:
                 log.debug('request.%s was not set on code validation.', attr)
 
         # REQUIRED, if the "redirect_uri" parameter was included in the
index 4a8017f..9fc632f 100644 (file)
@@ -23,7 +23,8 @@ class GrantTypeBase(object):
         raise NotImplementedError('Subclasses must implement this method.')
 
     def validate_grant_type(self, request):
-        if not self.request_validator.validate_grant_type(request.client_id,
+        client_id = getattr(request, 'client_id', None)
+        if not self.request_validator.validate_grant_type(client_id,
                                                           request.grant_type, request.client, request):
             log.debug('Unauthorized from %r (%r) access to grant type %s.',
                       request.client_id, request.client, request.grant_type)
index 30df247..49173cc 100644 (file)
@@ -83,7 +83,7 @@ class ClientCredentialsGrant(GrantTypeBase):
         return headers, json.dumps(token), 200
 
     def validate_token_request(self, request):
-        if not getattr(request, 'grant_type'):
+        if not getattr(request, 'grant_type', None):
             raise errors.InvalidRequestError('Request is missing grant type.',
                                              request=request)
 
index 27bcb24..2a92a02 100644 (file)
@@ -260,6 +260,15 @@ class ImplicitGrant(GrantTypeBase):
         # error and MUST NOT automatically redirect the user-agent to the
         # invalid redirection URI.
 
+        # First check duplicate parameters
+        for param in ('client_id', 'response_type', 'redirect_uri', 'scope', 'state'):
+            try:
+                duplicate_params = request.duplicate_params
+            except ValueError:
+                raise errors.InvalidRequestFatalError(description='Unable to parse query string', request=request)
+            if param in duplicate_params:
+                raise errors.InvalidRequestFatalError(description='Duplicate %s parameter.' % param, request=request)
+
         # REQUIRED. The client identifier as described in Section 2.2.
         # http://tools.ietf.org/html/rfc6749#section-2.2
         if not request.client_id:
@@ -304,23 +313,21 @@ class ImplicitGrant(GrantTypeBase):
         # http://tools.ietf.org/html/rfc6749#appendix-B
 
         # Note that the correct parameters to be added are automatically
-        # populated through the use of specific exceptions.
-        if request.response_type is None:
-            raise errors.InvalidRequestError(description='Missing response_type parameter.',
-                                             request=request)
+        # populated through the use of specific exceptions
 
-        for param in ('client_id', 'response_type', 'redirect_uri', 'scope', 'state'):
-            if param in request.duplicate_params:
-                raise errors.InvalidRequestError(description='Duplicate %s parameter.' % param, request=request)
-
-        # REQUIRED. Value MUST be set to "token".
-        if request.response_type != 'token':
+        # REQUIRED.
+        if request.response_type is None:
+            raise errors.MissingResponseTypeError(request=request)
+        # Value MUST be set to "token".
+        elif request.response_type != 'token':
             raise errors.UnsupportedResponseTypeError(request=request)
 
         log.debug('Validating use of response_type token for client %r (%r).',
                   request.client_id, request.client)
         if not self.request_validator.validate_response_type(request.client_id,
-                                                             request.response_type, request.client, request):
+                                                             request.response_type,
+                                                             request.client, request):
+
             log.debug('Client %s is not authorized to use response_type %s.',
                       request.client_id, request.response_type)
             raise errors.UnauthorizedClientError(request=request)
index c19e6cf..51b7374 100644 (file)
@@ -160,7 +160,7 @@ class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase):
         .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1
         """
         for param in ('grant_type', 'username', 'password'):
-            if not getattr(request, param):
+            if not getattr(request, param, None):
                 raise errors.InvalidRequestError(
                     'Request is missing %s parameter.' % param, request=request)
 
index e622ff1..442b2a8 100644 (file)
@@ -51,12 +51,6 @@ class RequestValidator(object):
         both body and query can be obtained by direct attribute access, i.e.
         request.client_id for client_id in the URL query.
 
-        OBS! Certain grant types rely on this authentication, possibly with
-        other fallbacks, and for them to recognize this authorization please
-        set the client attribute on the request (request.client). Note that
-        preferably this client object should have a client_id attribute of
-        unicode type (request.client.client_id).
-
         :param request: oauthlib.common.Request
         :rtype: True or False
 
@@ -90,14 +84,14 @@ class RequestValidator(object):
 
     def confirm_redirect_uri(self, client_id, code, redirect_uri, client,
                              *args, **kwargs):
-        """Ensure client is authorized to redirect to the redirect_uri requested.
-
-        If the client specifies a redirect_uri when obtaining code then
-        that redirect URI must be bound to the code and verified equal
-        in this method.
+        """Ensure that the authorization process represented by this authorization
+        code began with this 'redirect_uri'.
 
-        All clients should register the absolute URIs of all URIs they intend
-        to redirect to. The registration is outside of the scope of oauthlib.
+        If the client specifies a redirect_uri when obtaining code then that
+        redirect URI must be bound to the code and verified equal in this
+        method, according to RFC 6749 section 4.1.3.  Do not compare against
+        the client's allowed redirect URIs, but against the URI used when the
+        code was saved.
 
         :param client_id: Unicode client identifier
         :param code: Unicode authorization_code.
@@ -214,21 +208,25 @@ class RequestValidator(object):
     def save_authorization_code(self, client_id, code, request, *args, **kwargs):
         """Persist the authorization_code.
 
-        The code should at minimum be associated with:
-            - a client and it's client_id
+        The code should at minimum be stored with:
+            - the client_id (client_id)
             - the redirect URI used (request.redirect_uri)
-            - whether the redirect URI used is the client default or not
             - a resource owner / user (request.user)
-            - authorized scopes (request.scopes)
+            - the authorized scopes (request.scopes)
+            - the client state, if given (code.get('state'))
 
-        The authorization code grant dict (code) holds at least the key 'code'::
+        The 'code' argument is actually a dictionary, containing at least a
+        'code' key with the actual authorization code:
 
             {'code': 'sdf345jsdf0934f'}
 
+        It may also have a 'state' key containing a nonce for the client, if it
+        chose to send one.  That value should be saved and used in
+        'validate_code'.
+
         :param client_id: Unicode client identifier
-        :param code: A dict of the authorization code grant.
+        :param code: A dict of the authorization code grant and, optionally, state.
         :param request: The HTTP Request (oauthlib.common.Request)
-        :rtype: The default redirect URI for the client
 
         Method is used by:
             - Authorization Code Grant
@@ -339,11 +337,18 @@ class RequestValidator(object):
         raise NotImplementedError('Subclasses must implement this method.')
 
     def validate_code(self, client_id, code, client, request, *args, **kwargs):
-        """Ensure the authorization_code is valid and assigned to client.
+        """Verify that the authorization_code is valid and assigned to the given
+        client.
+
+        Before returning true, set the following based on the information stored
+        with the code in 'save_authorization_code':
 
+            - request.user
+            - request.state (if given)
+            - request.scopes
         OBS! The request.user attribute should be set to the resource owner
-        associated with this authorization code. Similarly request.scopes and
-        request.state must also be set.
+        associated with this authorization code. Similarly request.scopes
+        must also be set.
 
         :param client_id: Unicode client identifier
         :param code: Unicode authorization code
index 6a8e24b..870ac32 100644 (file)
@@ -24,20 +24,16 @@ def list_to_scope(scope):
     """Convert a list of scopes to a space separated string."""
     if isinstance(scope, unicode_type) or scope is None:
         return scope
-    elif isinstance(scope, (tuple, list)):
+    elif isinstance(scope, (set, tuple, list)):
         return " ".join([unicode_type(s) for s in scope])
-    elif isinstance(scope, set):
-        return list_to_scope(list(scope))
     else:
-        raise ValueError("Invalid scope, must be string or list.")
+        raise ValueError("Invalid scope (%s), must be string, tuple, set, or list." % scope)
 
 
 def scope_to_list(scope):
     """Convert a space separated string to a list of scopes."""
-    if isinstance(scope, list):
+    if isinstance(scope, (tuple, list, set)):
         return [unicode_type(s) for s in scope]
-    if isinstance(scope, set):
-        scope_to_list(list(scope))
     elif scope is None:
         return None
     else:

Benjamin Mako Hill || Want to submit a patch?