Skip to content

Commit d9c8f35

Browse files
authored
Merge pull request #1767 from praw-dev/refresh_token
Deprecate refresh token managers
2 parents a460e7e + d3e81a9 commit d9c8f35

File tree

11 files changed

+101
-74
lines changed

11 files changed

+101
-74
lines changed

CHANGES.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ Unreleased
1616
- :meth:`.update_crowd_control_level` to update the crowd control level of a post.
1717
- :meth:`.moderator_subreddits`, which returns information about the subreddits that the
1818
authenticated user moderates, has been restored.
19+
- The configuration setting ``refresh_token`` has been added back. See
20+
https://www.reddit.com/r/redditdev/comments/olk5e6/followup_oauth2_api_changes_regarding_refresh/
21+
for more info.
22+
23+
**Deprecated**
24+
25+
- :class:`.Reddit` keyword argument ``token_manager``.
1926

2027
7.3.0 (2021/06/17)
2128
------------------

docs/getting_started/authentication.rst

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,3 +249,29 @@ such as in installed applications where the end user could retrieve the ``client
249249
from each other (as the supplied device id *should* be a unique string per both
250250
device (in the case of a web app, server) and user (in the case of a web app,
251251
browser session).
252+
253+
.. _using_refresh_tokens:
254+
255+
Using a Saved Refresh Token
256+
---------------------------
257+
258+
A saved refresh token can be used to immediately obtain an authorized instance of
259+
:class:`.Reddit` like so:
260+
261+
.. code-block:: python
262+
263+
reddit = praw.Reddit(
264+
client_id="SI8pN3DSbt0zor",
265+
client_secret="xaxkj7HNh8kwg8e5t4m6KvSrbTI",
266+
refresh_token="WeheY7PwgeCZj4S3QgUcLhKE5S2s4eAYdxM",
267+
user_agent="testscript by u/fakebot3",
268+
)
269+
print(reddit.auth.scopes())
270+
271+
The output from the above code displays which scopes are available on the
272+
:class:`.Reddit` instance.
273+
274+
.. note::
275+
276+
Observe that ``redirect_uri`` does not need to be provided in such cases. It is only
277+
needed when :meth:`.url` is used.

docs/tutorials/refresh_token.rst

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,6 @@
33
Working with Refresh Tokens
44
===========================
55

6-
.. note::
7-
8-
The process for using refresh tokens is in the process of changing on Reddit's end.
9-
This documentation has been updated to be aligned with the future of how Reddit
10-
handles refresh tokens, and will be the only supported method in PRAW 8+. For more
11-
information please see:
12-
https://old.reddit.com/r/redditdev/comments/kvzaot/oauth2_api_changes_upcoming/
13-
146
Reddit OAuth2 Scopes
157
--------------------
168

@@ -82,33 +74,3 @@ The following program can be used to obtain a refresh token with the desired sco
8274

8375
.. literalinclude:: ../examples/obtain_refresh_token.py
8476
:language: python
85-
86-
.. _using_refresh_tokens:
87-
88-
Using and Updating Refresh Tokens
89-
---------------------------------
90-
91-
Reddit refresh tokens can be used only once. When an authorization is refreshed the
92-
existing refresh token is consumed and a new access token and refresh token will be
93-
issued. While PRAW automatically handles refreshing tokens when needed, it does not
94-
automatically handle the storage of the refresh tokens. However, PRAW provides the
95-
facilities for you to manage your refresh tokens via custom subclasses of
96-
:class:`.BaseTokenManager`. For trivial examples, PRAW provides the
97-
:class:`.FileTokenManager`.
98-
99-
The following program demonstrates how to prepare a file with an initial refresh token,
100-
and configure PRAW to both use that refresh token, and keep the file up-to-date with a
101-
valid refresh token.
102-
103-
.. literalinclude:: ../examples/use_file_token_manager.py
104-
:language: python
105-
106-
.. _sqlite_token_manager:
107-
108-
SQLiteTokenManager
109-
~~~~~~~~~~~~~~~~~~
110-
111-
For more complex examples, PRAW provides the :class:`.SQLiteTokenManager`.
112-
113-
.. literalinclude:: ../examples/use_sqlite_token_manager.py
114-
:language: python

praw/config.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import sys
55
from threading import Lock
66
from typing import Optional
7-
from warnings import warn
87

98
from .exceptions import ClientException
109

@@ -85,21 +84,11 @@ def __init__(
8584
self.custom = dict(Config.CONFIG.items(site_name), **settings)
8685

8786
self.client_id = self.client_secret = self.oauth_url = None
88-
self.reddit_url = self.redirect_uri = None
87+
self.reddit_url = self.refresh_token = self.redirect_uri = None
8988
self.password = self.user_agent = self.username = None
9089

9190
self._initialize_attributes()
9291

93-
self._do_not_use_refresh_token = self._fetch_or_not_set("refresh_token")
94-
if self._do_not_use_refresh_token != self.CONFIG_NOT_SET:
95-
warn(
96-
"The ``refresh_token`` configuration setting is deprecated and will be"
97-
" removed in PRAW 8. Please use ``token_manager`` to manage your"
98-
" refresh tokens.",
99-
category=DeprecationWarning,
100-
stacklevel=2,
101-
)
102-
10392
def _fetch(self, key):
10493
value = self.custom[key]
10594
del self.custom[key]
@@ -144,6 +133,7 @@ def _initialize_attributes(self):
144133
"client_id",
145134
"client_secret",
146135
"redirect_uri",
136+
"refresh_token",
147137
"password",
148138
"user_agent",
149139
"username",

praw/models/auth.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -105,20 +105,13 @@ def url(
105105
whom the URL was generated for.
106106
:param duration: Either ``permanent`` or ``temporary`` (default: permanent).
107107
``temporary`` authorizations generate access tokens that last only 1 hour.
108-
``permanent`` authorizations additionally generate a single-use refresh
109-
token with a significantly longer expiration (~1 year) that is to be used to
110-
fetch a new set of tokens. This value is ignored when ``implicit=True``.
108+
``permanent`` authorizations additionally generate a refresh token that
109+
expires 1 year after the last use and can be used indefinitely to generate
110+
new hour-long access tokens. This value is ignored when ``implicit=True``.
111111
:param implicit: For **installed** applications, this value can be set to use
112112
the implicit, rather than the code flow. When True, the ``duration``
113113
argument has no effect as only temporary tokens can be retrieved.
114114
115-
.. note::
116-
117-
Reddit's ``refresh_tokens`` currently are reusable, and do not expire.
118-
However, that behavior is likely to change in the near future so it's best
119-
to no longer rely upon it:
120-
https://old.reddit.com/r/redditdev/comments/kvzaot/oauth2_api_changes_upcoming/
121-
122115
"""
123116
authenticator = self._reddit._read_only_core._authorizer._authenticator
124117
if authenticator.redirect_uri is self._reddit.config.CONFIG_NOT_SET:

praw/reddit.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -420,9 +420,16 @@ def _check_for_update(self):
420420

421421
def _prepare_common_authorizer(self, authenticator):
422422
if self._token_manager is not None:
423-
if self.config._do_not_use_refresh_token != self.config.CONFIG_NOT_SET:
423+
warn(
424+
"Token managers have been depreciated and will be removed in the near"
425+
" future. See https://www.reddit.com/r/redditdev/comments/olk5e6/"
426+
"followup_oauth2_api_changes_regarding_refresh/ for more details.",
427+
category=DeprecationWarning,
428+
stacklevel=2,
429+
)
430+
if self.config.refresh_token:
424431
raise TypeError(
425-
"legacy ``refresh_token`` setting cannot be provided when providing"
432+
"``refresh_token`` setting cannot be provided when providing"
426433
" ``token_manager``"
427434
)
428435

@@ -432,9 +439,9 @@ def _prepare_common_authorizer(self, authenticator):
432439
post_refresh_callback=self._token_manager.post_refresh_callback,
433440
pre_refresh_callback=self._token_manager.pre_refresh_callback,
434441
)
435-
elif self.config._do_not_use_refresh_token != self.config.CONFIG_NOT_SET:
442+
elif self.config.refresh_token:
436443
authorizer = Authorizer(
437-
authenticator, refresh_token=self.config._do_not_use_refresh_token
444+
authenticator, refresh_token=self.config.refresh_token
438445
)
439446
else:
440447
self._core = self._read_only_core

praw/util/token_manager.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
A few proof of concept token manager classes are provided here, but it is expected that
77
PRAW users will create their own token manager classes suitable for their needs.
88
9-
See :ref:`using_refresh_tokens` for examples on how to leverage these classes.
9+
.. deprecated:: 7.4.0
10+
11+
Tokens managers have been depreciated and will be removed in the near future.
1012
1113
"""
1214
import sqlite3
@@ -100,7 +102,6 @@ class SQLiteTokenManager(BaseTokenManager):
100102
Unlike, :class:`.FileTokenManager`, the initial database need not be created ahead
101103
of time, as it'll automatically be created on first use. However, initial
102104
``refresh_tokens`` will need to be registered via :meth:`.register` prior to use.
103-
See :ref:`sqlite_token_manager` for an example of use.
104105
105106
.. warning::
106107

tests/conftest.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
import socket
55
import time
66
from base64 import b64encode
7+
from functools import wraps
78
from sys import platform
89
from urllib.parse import quote_plus
910

1011
import betamax
1112
import pytest
13+
from betamax.cassette.cassette import Cassette, dispatch_hooks
1214
from betamax.serializers import JSONSerializer
1315

1416

@@ -55,7 +57,7 @@ def filter_access_token(interaction, current_cassette):
5557
x: env_default(x)
5658
for x in (
5759
"auth_code client_id client_secret password redirect_uri test_subreddit"
58-
" user_agent username"
60+
" user_agent username refresh_token"
5961
).split()
6062
}
6163

@@ -83,6 +85,28 @@ def serialize(self, cassette_data):
8385
config.define_cassette_placeholder(f"<{key.upper()}>", value)
8486

8587

88+
def add_init_hook(original_init):
89+
"""Wrap an __init__ method to also call some hooks."""
90+
91+
@wraps(original_init)
92+
def wrapper(self, *args, **kwargs):
93+
original_init(self, *args, **kwargs)
94+
dispatch_hooks("after_init", self)
95+
96+
return wrapper
97+
98+
99+
Cassette.__init__ = add_init_hook(Cassette.__init__)
100+
101+
102+
def init_hook(cassette):
103+
if cassette.is_recording():
104+
pytest.set_up_record() # dynamically defined in __init__.py
105+
106+
107+
Cassette.hooks["after_init"].append(init_hook)
108+
109+
86110
class Placeholders:
87111
def __init__(self, _dict):
88112
self.__dict__ = _dict

tests/integration/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class IntegrationTest:
1616

1717
def setup(self):
1818
"""Setup runs before all test cases."""
19+
self._overrode_reddit_setup = True
1920
self.setup_reddit()
2021
self.setup_betamax()
2122

@@ -31,7 +32,11 @@ def setup_betamax(self):
3132
# Require tests to explicitly disable read_only mode.
3233
self.reddit.read_only = True
3334

35+
pytest.set_up_record = self.set_up_record # used in conftest.py
36+
3437
def setup_reddit(self):
38+
self._overrode_reddit_setup = False
39+
3540
self._session = requests.Session()
3641

3742
self.reddit = Reddit(
@@ -43,6 +48,17 @@ def setup_reddit(self):
4348
username=pytest.placeholders.username,
4449
)
4550

51+
def set_up_record(self):
52+
if not self._overrode_reddit_setup:
53+
if pytest.placeholders.refresh_token != "placeholder_refresh_token":
54+
self.reddit = Reddit(
55+
requestor_kwargs={"session": self._session},
56+
client_id=pytest.placeholders.client_id,
57+
client_secret=pytest.placeholders.client_secret,
58+
user_agent=pytest.placeholders.user_agent,
59+
refresh_token=pytest.placeholders.refresh_token,
60+
)
61+
4662
def use_cassette(self, cassette_name=None, **kwargs):
4763
"""Use a cassette. The cassette name is dynamically generated.
4864

tests/unit/test_deprecations.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from praw import Reddit
66
from praw.exceptions import APIException, WebSocketException
77
from praw.models.reddit.user_subreddit import UserSubreddit
8+
from praw.util.token_manager import FileTokenManager
89

910
from . import UnitTest
1011

@@ -59,20 +60,20 @@ def test_gild_method(self):
5960
self.reddit.submission("1234").gild()
6061
assert excinfo.value.args[0] == "`.gild` has been renamed to `.award`."
6162

62-
def test_reddit_user_me_read_only(self):
63-
with pytest.raises(DeprecationWarning):
64-
self.reddit.user.me()
65-
66-
def test_reddit_refresh_token(self):
63+
def test_reddit_token_manager(self):
6764
with pytest.raises(DeprecationWarning):
6865
Reddit(
6966
client_id="dummy",
7067
client_secret=None,
7168
redirect_uri="dummy",
72-
refresh_token="dummy",
7369
user_agent="dummy",
70+
token_manager=FileTokenManager("name"),
7471
)
7572

73+
def test_reddit_user_me_read_only(self):
74+
with pytest.raises(DeprecationWarning):
75+
self.reddit.user.me()
76+
7677
def test_user_subreddit_as_dict(self):
7778
user_subreddit = UserSubreddit(None, display_name="test")
7879
with pytest.deprecated_call() as warning_info:

0 commit comments

Comments
 (0)