Skip to content

Commit

Permalink
Merge pull request #1767 from praw-dev/refresh_token
Browse files Browse the repository at this point in the history
Deprecate refresh token managers
  • Loading branch information
LilSpazJoekp authored Jul 23, 2021
2 parents a460e7e + d3e81a9 commit d9c8f35
Show file tree
Hide file tree
Showing 11 changed files with 101 additions and 74 deletions.
7 changes: 7 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ Unreleased
- :meth:`.update_crowd_control_level` to update the crowd control level of a post.
- :meth:`.moderator_subreddits`, which returns information about the subreddits that the
authenticated user moderates, has been restored.
- The configuration setting ``refresh_token`` has been added back. See
https://www.reddit.com/r/redditdev/comments/olk5e6/followup_oauth2_api_changes_regarding_refresh/
for more info.

**Deprecated**

- :class:`.Reddit` keyword argument ``token_manager``.

7.3.0 (2021/06/17)
------------------
Expand Down
26 changes: 26 additions & 0 deletions docs/getting_started/authentication.rst
Original file line number Diff line number Diff line change
Expand Up @@ -249,3 +249,29 @@ such as in installed applications where the end user could retrieve the ``client
from each other (as the supplied device id *should* be a unique string per both
device (in the case of a web app, server) and user (in the case of a web app,
browser session).

.. _using_refresh_tokens:

Using a Saved Refresh Token
---------------------------

A saved refresh token can be used to immediately obtain an authorized instance of
:class:`.Reddit` like so:

.. code-block:: python
reddit = praw.Reddit(
client_id="SI8pN3DSbt0zor",
client_secret="xaxkj7HNh8kwg8e5t4m6KvSrbTI",
refresh_token="WeheY7PwgeCZj4S3QgUcLhKE5S2s4eAYdxM",
user_agent="testscript by u/fakebot3",
)
print(reddit.auth.scopes())
The output from the above code displays which scopes are available on the
:class:`.Reddit` instance.

.. note::

Observe that ``redirect_uri`` does not need to be provided in such cases. It is only
needed when :meth:`.url` is used.
38 changes: 0 additions & 38 deletions docs/tutorials/refresh_token.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,6 @@
Working with Refresh Tokens
===========================

.. note::

The process for using refresh tokens is in the process of changing on Reddit's end.
This documentation has been updated to be aligned with the future of how Reddit
handles refresh tokens, and will be the only supported method in PRAW 8+. For more
information please see:
https://old.reddit.com/r/redditdev/comments/kvzaot/oauth2_api_changes_upcoming/

Reddit OAuth2 Scopes
--------------------

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

.. literalinclude:: ../examples/obtain_refresh_token.py
:language: python

.. _using_refresh_tokens:

Using and Updating Refresh Tokens
---------------------------------

Reddit refresh tokens can be used only once. When an authorization is refreshed the
existing refresh token is consumed and a new access token and refresh token will be
issued. While PRAW automatically handles refreshing tokens when needed, it does not
automatically handle the storage of the refresh tokens. However, PRAW provides the
facilities for you to manage your refresh tokens via custom subclasses of
:class:`.BaseTokenManager`. For trivial examples, PRAW provides the
:class:`.FileTokenManager`.

The following program demonstrates how to prepare a file with an initial refresh token,
and configure PRAW to both use that refresh token, and keep the file up-to-date with a
valid refresh token.

.. literalinclude:: ../examples/use_file_token_manager.py
:language: python

.. _sqlite_token_manager:

SQLiteTokenManager
~~~~~~~~~~~~~~~~~~

For more complex examples, PRAW provides the :class:`.SQLiteTokenManager`.

.. literalinclude:: ../examples/use_sqlite_token_manager.py
:language: python
14 changes: 2 additions & 12 deletions praw/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import sys
from threading import Lock
from typing import Optional
from warnings import warn

from .exceptions import ClientException

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

self.client_id = self.client_secret = self.oauth_url = None
self.reddit_url = self.redirect_uri = None
self.reddit_url = self.refresh_token = self.redirect_uri = None
self.password = self.user_agent = self.username = None

self._initialize_attributes()

self._do_not_use_refresh_token = self._fetch_or_not_set("refresh_token")
if self._do_not_use_refresh_token != self.CONFIG_NOT_SET:
warn(
"The ``refresh_token`` configuration setting is deprecated and will be"
" removed in PRAW 8. Please use ``token_manager`` to manage your"
" refresh tokens.",
category=DeprecationWarning,
stacklevel=2,
)

def _fetch(self, key):
value = self.custom[key]
del self.custom[key]
Expand Down Expand Up @@ -144,6 +133,7 @@ def _initialize_attributes(self):
"client_id",
"client_secret",
"redirect_uri",
"refresh_token",
"password",
"user_agent",
"username",
Expand Down
13 changes: 3 additions & 10 deletions praw/models/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,20 +105,13 @@ def url(
whom the URL was generated for.
:param duration: Either ``permanent`` or ``temporary`` (default: permanent).
``temporary`` authorizations generate access tokens that last only 1 hour.
``permanent`` authorizations additionally generate a single-use refresh
token with a significantly longer expiration (~1 year) that is to be used to
fetch a new set of tokens. This value is ignored when ``implicit=True``.
``permanent`` authorizations additionally generate a refresh token that
expires 1 year after the last use and can be used indefinitely to generate
new hour-long access tokens. This value is ignored when ``implicit=True``.
:param implicit: For **installed** applications, this value can be set to use
the implicit, rather than the code flow. When True, the ``duration``
argument has no effect as only temporary tokens can be retrieved.
.. note::
Reddit's ``refresh_tokens`` currently are reusable, and do not expire.
However, that behavior is likely to change in the near future so it's best
to no longer rely upon it:
https://old.reddit.com/r/redditdev/comments/kvzaot/oauth2_api_changes_upcoming/
"""
authenticator = self._reddit._read_only_core._authorizer._authenticator
if authenticator.redirect_uri is self._reddit.config.CONFIG_NOT_SET:
Expand Down
15 changes: 11 additions & 4 deletions praw/reddit.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,9 +420,16 @@ def _check_for_update(self):

def _prepare_common_authorizer(self, authenticator):
if self._token_manager is not None:
if self.config._do_not_use_refresh_token != self.config.CONFIG_NOT_SET:
warn(
"Token managers have been depreciated and will be removed in the near"
" future. See https://www.reddit.com/r/redditdev/comments/olk5e6/"
"followup_oauth2_api_changes_regarding_refresh/ for more details.",
category=DeprecationWarning,
stacklevel=2,
)
if self.config.refresh_token:
raise TypeError(
"legacy ``refresh_token`` setting cannot be provided when providing"
"``refresh_token`` setting cannot be provided when providing"
" ``token_manager``"
)

Expand All @@ -432,9 +439,9 @@ def _prepare_common_authorizer(self, authenticator):
post_refresh_callback=self._token_manager.post_refresh_callback,
pre_refresh_callback=self._token_manager.pre_refresh_callback,
)
elif self.config._do_not_use_refresh_token != self.config.CONFIG_NOT_SET:
elif self.config.refresh_token:
authorizer = Authorizer(
authenticator, refresh_token=self.config._do_not_use_refresh_token
authenticator, refresh_token=self.config.refresh_token
)
else:
self._core = self._read_only_core
Expand Down
5 changes: 3 additions & 2 deletions praw/util/token_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
A few proof of concept token manager classes are provided here, but it is expected that
PRAW users will create their own token manager classes suitable for their needs.
See :ref:`using_refresh_tokens` for examples on how to leverage these classes.
.. deprecated:: 7.4.0
Tokens managers have been depreciated and will be removed in the near future.
"""
import sqlite3
Expand Down Expand Up @@ -100,7 +102,6 @@ class SQLiteTokenManager(BaseTokenManager):
Unlike, :class:`.FileTokenManager`, the initial database need not be created ahead
of time, as it'll automatically be created on first use. However, initial
``refresh_tokens`` will need to be registered via :meth:`.register` prior to use.
See :ref:`sqlite_token_manager` for an example of use.
.. warning::
Expand Down
26 changes: 25 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
import socket
import time
from base64 import b64encode
from functools import wraps
from sys import platform
from urllib.parse import quote_plus

import betamax
import pytest
from betamax.cassette.cassette import Cassette, dispatch_hooks
from betamax.serializers import JSONSerializer


Expand Down Expand Up @@ -55,7 +57,7 @@ def filter_access_token(interaction, current_cassette):
x: env_default(x)
for x in (
"auth_code client_id client_secret password redirect_uri test_subreddit"
" user_agent username"
" user_agent username refresh_token"
).split()
}

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


def add_init_hook(original_init):
"""Wrap an __init__ method to also call some hooks."""

@wraps(original_init)
def wrapper(self, *args, **kwargs):
original_init(self, *args, **kwargs)
dispatch_hooks("after_init", self)

return wrapper


Cassette.__init__ = add_init_hook(Cassette.__init__)


def init_hook(cassette):
if cassette.is_recording():
pytest.set_up_record() # dynamically defined in __init__.py


Cassette.hooks["after_init"].append(init_hook)


class Placeholders:
def __init__(self, _dict):
self.__dict__ = _dict
Expand Down
16 changes: 16 additions & 0 deletions tests/integration/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class IntegrationTest:

def setup(self):
"""Setup runs before all test cases."""
self._overrode_reddit_setup = True
self.setup_reddit()
self.setup_betamax()

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

pytest.set_up_record = self.set_up_record # used in conftest.py

def setup_reddit(self):
self._overrode_reddit_setup = False

self._session = requests.Session()

self.reddit = Reddit(
Expand All @@ -43,6 +48,17 @@ def setup_reddit(self):
username=pytest.placeholders.username,
)

def set_up_record(self):
if not self._overrode_reddit_setup:
if pytest.placeholders.refresh_token != "placeholder_refresh_token":
self.reddit = Reddit(
requestor_kwargs={"session": self._session},
client_id=pytest.placeholders.client_id,
client_secret=pytest.placeholders.client_secret,
user_agent=pytest.placeholders.user_agent,
refresh_token=pytest.placeholders.refresh_token,
)

def use_cassette(self, cassette_name=None, **kwargs):
"""Use a cassette. The cassette name is dynamically generated.
Expand Down
13 changes: 7 additions & 6 deletions tests/unit/test_deprecations.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from praw import Reddit
from praw.exceptions import APIException, WebSocketException
from praw.models.reddit.user_subreddit import UserSubreddit
from praw.util.token_manager import FileTokenManager

from . import UnitTest

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

def test_reddit_user_me_read_only(self):
with pytest.raises(DeprecationWarning):
self.reddit.user.me()

def test_reddit_refresh_token(self):
def test_reddit_token_manager(self):
with pytest.raises(DeprecationWarning):
Reddit(
client_id="dummy",
client_secret=None,
redirect_uri="dummy",
refresh_token="dummy",
user_agent="dummy",
token_manager=FileTokenManager("name"),
)

def test_reddit_user_me_read_only(self):
with pytest.raises(DeprecationWarning):
self.reddit.user.me()

def test_user_subreddit_as_dict(self):
user_subreddit = UserSubreddit(None, display_name="test")
with pytest.deprecated_call() as warning_info:
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_reddit.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def test_conflicting_settings(self):
)
assert (
str(excinfo.value)
== "legacy ``refresh_token`` setting cannot be provided when providing ``token_manager``"
== "``refresh_token`` setting cannot be provided when providing ``token_manager``"
)

def test_context_manager(self):
Expand Down

0 comments on commit d9c8f35

Please sign in to comment.