diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c627465..fbc4d3f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,6 +60,6 @@ jobs: run: coveralls strategy: matrix: - python-version: [3.6, 3.7, 3.8] + python-version: [3.6, 3.7, 3.8, 3.9] name: CI on: [pull_request, push] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bedfa07..f00afa7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,8 +1,7 @@ -default_language_version: - python: python3.7 fail_fast: true repos: - - hooks: - - id: black - repo: https://github.com/ambv/black - rev: stable +- hooks: + - id: black + language_version: python3 + repo: https://github.com/psf/black + rev: stable diff --git a/CHANGES.rst b/CHANGES.rst index 7d9e6a0..2f4ac4b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,23 @@ Change Log prawcore follows `semantic versioning `_ with the exception that deprecations will not be announced by a minor release. +Unreleased +---------- + +**Added** + +* ``Authorizer`` optionally takes a ``pre_refresh_callback`` keyword + argument. If provided, the function will called with the instance of + ``Authorizer`` prior to refreshing the access and refresh tokens. +* ``Authorizer`` optionally takes a ``post_refresh_callback`` keyword + argument. If provided, the function will called with the instance of + ``Authorizer`` after refreshing the access and refresh tokens. + +**Changed** + +* The ``refresh_token`` argument to ``Authorizer`` must now be passed by + keyword, and cannot be passed as a positional argument. + 1.5.0 (2020-08-04) ------------------ @@ -16,8 +33,8 @@ that deprecations will not be announced by a minor release. **Added** -* When calling :meth:`.Session.request`, we add the key-value pair - ``"api_type": "json"`` to the ``json`` parameter, if it is a ``dict``. +* When calling ``Session.request``, we add the key-value pair ``"api_type": + "json"`` to the ``json`` parameter, if it is a ``dict``. **Changed** @@ -31,8 +48,8 @@ that deprecations will not be announced by a minor release. **Added** -* All other requestor methods, most notably :meth:`.Session.request`, now contain - a ``timeout`` parameter. +* All other requestor methods, most notably ``Session.request``, now contain a + ``timeout`` parameter. 1.2.0 (2020-04-23) @@ -40,16 +57,13 @@ that deprecations will not be announced by a minor release. **Added** -* Method ``Requestor.request`` can be given a timeout parameter to - control the amount of time to wait for a request to succeed. +* Method ``Requestor.request`` can be given a timeout parameter to control the + amount of time to wait for a request to succeed. **Changed** * Updated rate limit algorithm to more intelligently rate limit when there are extra requests remaining. - -**Removed** - * Drop python 2.7 support. 1.0.1 (2019-02-05) diff --git a/examples/obtain_refresh_token.py b/examples/obtain_refresh_token.py index 9ddd32d..c58a91e 100755 --- a/examples/obtain_refresh_token.py +++ b/examples/obtain_refresh_token.py @@ -48,7 +48,7 @@ def main(): prawcore.Requestor("prawcore_refresh_token_example"), os.environ["PRAWCORE_CLIENT_ID"], os.environ["PRAWCORE_CLIENT_SECRET"], - os.environ["PRAWCORE_REDIRECT_URI"], + "http://localhost:8080", ) state = str(random.randint(0, 65000)) diff --git a/prawcore/auth.py b/prawcore/auth.py index d8ca5b4..8074175 100644 --- a/prawcore/auth.py +++ b/prawcore/auth.py @@ -199,16 +199,35 @@ class Authorizer(BaseAuthorizer): AUTHENTICATOR_CLASS = BaseAuthenticator - def __init__(self, authenticator, refresh_token=None): + def __init__( + self, + authenticator, + *, + post_refresh_callback=None, + pre_refresh_callback=None, + refresh_token=None + ): """Represent a single authorization to Reddit's API. :param authenticator: An instance of a subclass of :class:`BaseAuthenticator`. + :param post_refresh_callback: (Optional) When a single-argument + function is passed, the function will be called prior to refreshing + the access and refresh tokens. The argument to the callback is the + :class:`Authorizor` instance. This callback can be used to inspect + and modify the attributes of the :class:`Authorizor`. + :param pre_refresh_callback: (Optional) When a single-argument function + is passed, the function will be called after refreshing the access + and refresh tokens. The argument to the callback is the + :class:`Authorizor` instance. This callback can be used to inspect + and modify the attributes of the :class:`Authorizor`. :param refresh_token: (Optional) Enables the ability to refresh the authorization. """ super(Authorizer, self).__init__(authenticator) + self._post_refresh_callback = post_refresh_callback + self._pre_refresh_callback = pre_refresh_callback self.refresh_token = refresh_token def authorize(self, code): @@ -228,11 +247,15 @@ def authorize(self, code): def refresh(self): """Obtain a new access token from the refresh_token.""" + if self._pre_refresh_callback: + self._pre_refresh_callback(self) if self.refresh_token is None: raise InvalidInvocation("refresh token not provided") self._request_token( grant_type="refresh_token", refresh_token=self.refresh_token ) + if self._post_refresh_callback: + self._post_refresh_callback(self) def revoke(self, only_access=False): """Revoke the current Authorization. diff --git a/prawcore/sessions.py b/prawcore/sessions.py index 054b019..45ad7fa 100644 --- a/prawcore/sessions.py +++ b/prawcore/sessions.py @@ -202,8 +202,11 @@ def _make_request( ) return response, None except RequestException as exception: - if not retry_strategy_state.should_retry_on_failure() or not isinstance( # noqa: E501 - exception.original_exception, self.RETRY_EXCEPTIONS + if ( + not retry_strategy_state.should_retry_on_failure() + or not isinstance( # noqa: E501 + exception.original_exception, self.RETRY_EXCEPTIONS + ) ): raise return None, exception.original_exception diff --git a/setup.py b/setup.py index 4853aa7..f63b79d 100644 --- a/setup.py +++ b/setup.py @@ -42,6 +42,7 @@ "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", ], description="Low-level communication layer for PRAW 4+.", extras_require=extras, diff --git a/tests/test_authorizer.py b/tests/test_authorizer.py index 674429d..52b4bc4 100644 --- a/tests/test_authorizer.py +++ b/tests/test_authorizer.py @@ -77,7 +77,9 @@ def test_initialize(self): self.assertFalse(authorizer.is_valid()) def test_initialize__with_refresh_token(self): - authorizer = prawcore.Authorizer(self.authentication, REFRESH_TOKEN) + authorizer = prawcore.Authorizer( + self.authentication, refresh_token=REFRESH_TOKEN + ) self.assertIsNone(authorizer.access_token) self.assertIsNone(authorizer.scopes) self.assertEqual(REFRESH_TOKEN, authorizer.refresh_token) @@ -92,7 +94,46 @@ def test_initialize__with_untrusted_authenticator(self): self.assertFalse(authorizer.is_valid()) def test_refresh(self): - authorizer = prawcore.Authorizer(self.authentication, REFRESH_TOKEN) + authorizer = prawcore.Authorizer( + self.authentication, refresh_token=REFRESH_TOKEN + ) + with Betamax(REQUESTOR).use_cassette("Authorizer_refresh"): + authorizer.refresh() + + self.assertIsNotNone(authorizer.access_token) + self.assertIsInstance(authorizer.scopes, set) + self.assertTrue(len(authorizer.scopes) > 0) + self.assertTrue(authorizer.is_valid()) + + def test_refresh__post_refresh_callback(self): + def callback(authorizer): + # Reddit isn't currently returning new refresh_tokens so + # the following assertion is currently commented out. + # self.assertNotEqual(REFRESH_TOKEN, authorizer.refresh_token) + authorizer.refresh_token = "manually_updated" + + authorizer = prawcore.Authorizer( + self.authentication, + post_refresh_callback=callback, + refresh_token=REFRESH_TOKEN, + ) + with Betamax(REQUESTOR).use_cassette("Authorizer_refresh"): + authorizer.refresh() + + self.assertIsNotNone(authorizer.access_token) + self.assertEqual("manually_updated", authorizer.refresh_token) + self.assertIsInstance(authorizer.scopes, set) + self.assertTrue(len(authorizer.scopes) > 0) + self.assertTrue(authorizer.is_valid()) + + def test_refresh__pre_refresh_callback(self): + def callback(authorizer): + self.assertIsNone(authorizer.refresh_token) + authorizer.refresh_token = REFRESH_TOKEN + + authorizer = prawcore.Authorizer( + self.authentication, pre_refresh_callback=callback + ) with Betamax(REQUESTOR).use_cassette("Authorizer_refresh"): authorizer.refresh() @@ -102,7 +143,9 @@ def test_refresh(self): self.assertTrue(authorizer.is_valid()) def test_refresh__with_invalid_token(self): - authorizer = prawcore.Authorizer(self.authentication, "INVALID_TOKEN") + authorizer = prawcore.Authorizer( + self.authentication, refresh_token="INVALID_TOKEN" + ) with Betamax(REQUESTOR).use_cassette( "Authorizer_refresh__with_invalid_token" ): @@ -115,7 +158,9 @@ def test_refresh__without_refresh_token(self): self.assertFalse(authorizer.is_valid()) def test_revoke__access_token_with_refresh_set(self): - authorizer = prawcore.Authorizer(self.authentication, REFRESH_TOKEN) + authorizer = prawcore.Authorizer( + self.authentication, refresh_token=REFRESH_TOKEN + ) with Betamax(REQUESTOR).use_cassette( "Authorizer_revoke__access_token_with_refresh_set" ): @@ -146,7 +191,9 @@ def test_revoke__access_token_without_refresh_set(self): self.assertFalse(authorizer.is_valid()) def test_revoke__refresh_token_with_access_set(self): - authorizer = prawcore.Authorizer(self.authentication, REFRESH_TOKEN) + authorizer = prawcore.Authorizer( + self.authentication, refresh_token=REFRESH_TOKEN + ) with Betamax(REQUESTOR).use_cassette( "Authorizer_revoke__refresh_token_with_access_set" ): @@ -159,7 +206,9 @@ def test_revoke__refresh_token_with_access_set(self): self.assertFalse(authorizer.is_valid()) def test_revoke__refresh_token_without_access_set(self): - authorizer = prawcore.Authorizer(self.authentication, REFRESH_TOKEN) + authorizer = prawcore.Authorizer( + self.authentication, refresh_token=REFRESH_TOKEN + ) with Betamax(REQUESTOR).use_cassette( "Authorizer_revoke__refresh_token_without_access_set" ): @@ -171,7 +220,9 @@ def test_revoke__refresh_token_without_access_set(self): self.assertFalse(authorizer.is_valid()) def test_revoke__without_access_token(self): - authorizer = prawcore.Authorizer(self.authentication, REFRESH_TOKEN) + authorizer = prawcore.Authorizer( + self.authentication, refresh_token=REFRESH_TOKEN + ) self.assertRaises( prawcore.InvalidInvocation, authorizer.revoke, only_access=True ) diff --git a/tests/test_sessions.py b/tests/test_sessions.py index 5e5d37d..a7ecff4 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -38,7 +38,9 @@ def client_authorizer(): authenticator = prawcore.TrustedAuthenticator( REQUESTOR, CLIENT_ID, CLIENT_SECRET ) - authorizer = prawcore.Authorizer(authenticator, REFRESH_TOKEN) + authorizer = prawcore.Authorizer( + authenticator, refresh_token=REFRESH_TOKEN + ) authorizer.refresh() return authorizer