Skip to content

Commit

Permalink
Merge b07604a into 181a28a
Browse files Browse the repository at this point in the history
  • Loading branch information
LilSpazJoekp committed Jul 27, 2021
2 parents 181a28a + b07604a commit 16037ec
Show file tree
Hide file tree
Showing 12 changed files with 130 additions and 87 deletions.
8 changes: 8 additions & 0 deletions CHANGES.rst
Expand Up @@ -6,6 +6,14 @@ Async PRAW follows `semantic versioning <http://semver.org/>`_.
Unreleased
----------

- 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.1 (2021/07/06)
------------------

Expand Down
14 changes: 2 additions & 12 deletions asyncpraw/config.py
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 Async 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 @@ -141,6 +130,7 @@ def _initialize_attributes(self):
"client_id",
"client_secret",
"redirect_uri",
"refresh_token",
"password",
"user_agent",
"username",
Expand Down
11 changes: 2 additions & 9 deletions asyncpraw/models/auth.py
Expand Up @@ -111,20 +111,13 @@ def url(
:param duration: Either ``permanent`` or ``temporary`` (default: permanent).
``temporary`` authorizations generate access tokens that last only 1 hour.
``permanent`` authorizations additionally ``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
additionally generate a refresh token that expires 1 year after the last use
and can be used indefinitelyto 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 asyncpraw/reddit.py
Expand Up @@ -458,9 +458,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 @@ -470,9 +477,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 asyncpraw/util/token_manager.py
Expand Up @@ -6,7 +6,9 @@
A few proof of concept token manager classes are provided here, but it is expected that
Async 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.
"""
from abc import ABC, abstractmethod
Expand Down Expand Up @@ -103,7 +105,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: 26 additions & 0 deletions docs/getting_started/authentication.rst
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
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
27 changes: 26 additions & 1 deletion tests/conftest.py
Expand Up @@ -4,10 +4,12 @@
import os
from base64 import b64encode
from datetime import datetime
from functools import wraps

import pytest
from _pytest.tmpdir import _mk_tmp
from vcr import VCR
from vcr.cassette import Cassette
from vcr.persisters.filesystem import FilesystemPersister
from vcr.serialize import deserialize, serialize

Expand Down Expand Up @@ -87,7 +89,7 @@ def serialize_list(data: list):
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 @@ -157,6 +159,29 @@ def use_cassette(self, path="", **kwargs):
VCR.register_persister(CustomPersister)


def after_init(func, *args):
func(*args)


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)
after_init(init_hook, self)

return wrapper


Cassette.__init__ = add_init_hook(Cassette.__init__)


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


class Placeholders:
def __init__(self, _dict):
self.__dict__ = _dict
Expand Down
41 changes: 33 additions & 8 deletions tests/integration/__init__.py
Expand Up @@ -17,6 +17,7 @@ class IntegrationTest(asynctest.TestCase):

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

Expand All @@ -36,16 +37,40 @@ def setup_vcr(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 = aiohttp.ClientSession()
self.reddit = Reddit(
requestor_kwargs={"session": self._session},
client_id=pytest.placeholders.client_id,
client_secret=pytest.placeholders.client_secret,
password=pytest.placeholders.password,
user_agent=pytest.placeholders.user_agent,
username=pytest.placeholders.username,
)
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,
)
else:
self.reddit = Reddit(
requestor_kwargs={"session": self._session},
client_id=pytest.placeholders.client_id,
client_secret=pytest.placeholders.client_secret,
password=pytest.placeholders.password,
user_agent=pytest.placeholders.user_agent,
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,
)

@staticmethod
async def async_list(async_generator):
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/__init__.py
Expand Up @@ -13,5 +13,5 @@ async def setUp(self):
client_id="dummy", client_secret="dummy", user_agent="dummy"
)
# Unit tests should never issue requests
await self.reddit._core.close()
await self.reddit.close()
self.reddit._core._requestor._http = None
28 changes: 17 additions & 11 deletions tests/unit/test_deprecations.py
Expand Up @@ -6,9 +6,14 @@
from asyncpraw.exceptions import APIException, AsyncPRAWException, WebSocketException
from asyncpraw.models import Subreddit
from asyncpraw.models.reddit.user_subreddit import UserSubreddit
from asyncpraw.util.token_manager import FileTokenManager

from . import UnitTest

pytestmark = pytest.mark.filterwarnings(
"ignore:Unclosed client session", category=ResourceWarning
)


@pytest.mark.filterwarnings("error", category=DeprecationWarning)
class TestDeprecation(UnitTest):
Expand Down Expand Up @@ -83,7 +88,18 @@ async def test_reddit_user_me_read_only(self):
with pytest.raises(DeprecationWarning):
await self.reddit.user.me()

def test_synchronous_context_manager(self):
async def test_reddit_token_manager(self):
with pytest.raises(DeprecationWarning):
async with Reddit(
client_id="dummy",
client_secret=None,
redirect_uri="dummy",
user_agent="dummy",
token_manager=FileTokenManager("name"),
) as reddit:
reddit._core._requestor._http = None

async def test_synchronous_context_manager(self):
with pytest.raises(DeprecationWarning) as excinfo:
with self.reddit:
pass
Expand All @@ -92,16 +108,6 @@ def test_synchronous_context_manager(self):
== "Using this class as a synchronous context manager is deprecated and will be removed in the next release. Use this class as an asynchronous context manager instead."
)

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

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
Expand Up @@ -64,7 +64,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``"
)

async def test_context_manager(self):
Expand Down

0 comments on commit 16037ec

Please sign in to comment.