Skip to content

Commit

Permalink
Merge ea9f683 into da7fddb
Browse files Browse the repository at this point in the history
  • Loading branch information
LilSpazJoekp committed Feb 25, 2021
2 parents da7fddb + ea9f683 commit b455d23
Show file tree
Hide file tree
Showing 19 changed files with 545 additions and 173 deletions.
12 changes: 12 additions & 0 deletions CHANGES.rst
Expand Up @@ -4,8 +4,20 @@ Change Log
Unreleased
----------

**Added**

* :class:`.Reddit` keyword argument ``token_manager``.
* :class:`.FileTokenManager` and its parent abstract class :class:`.BaseTokenManager`.

**Deprecated**

* The configuration setting ``refresh_token`` is deprecated and its use will result in a
:py:class:`DeprecationWarning`. This deprecation applies in all ways of setting
configuration values, i.e., via ``praw.ini``, as a keyword argument when initializing
an instance of :class:`.Reddit`, and via the ``PRAW_REFRESH_TOKEN`` environment
variable. To be prepared for Async PRAW 8, use the new :class:`.Reddit` keyword
argument ``token_manager``. See :ref:`refresh_token` in Async PRAW's documentation for
an example.
* :meth:`.me` will no longer return ``None`` when called in :attr:`.read_only` mode
starting in Async PRAW 8. A :py:class:`DeprecationWarning` will be issued. To switch
forward to the Async PRAW 8 behavior set ``praw8_raise_exception_on_me=True`` in your
Expand Down
14 changes: 12 additions & 2 deletions asyncpraw/config.py
Expand Up @@ -4,6 +4,7 @@
import sys
from threading import Lock
from typing import Optional
from warnings import warn

from .exceptions import ClientException

Expand Down Expand Up @@ -84,11 +85,21 @@ 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.refresh_token = self.redirect_uri = None
self.reddit_url = 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 @@ -130,7 +141,6 @@ def _initialize_attributes(self):
"client_id",
"client_secret",
"redirect_uri",
"refresh_token",
"password",
"user_agent",
"username",
Expand Down
13 changes: 10 additions & 3 deletions asyncpraw/models/auth.py
Expand Up @@ -111,14 +111,21 @@ def url(
:param duration: Either ``permanent`` or ``temporary`` (default:
permanent). ``temporary`` authorizations generate access tokens
that last only 1 hour. ``permanent`` authorizations additionally
generate a refresh token that can be indefinitely used to generate
new hour-long access tokens. This value is ignored when
``implicit=True``.
``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``.
: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
42 changes: 33 additions & 9 deletions asyncpraw/reddit.py
Expand Up @@ -40,6 +40,7 @@
RedditAPIException,
)
from .objector import Objector
from .util.token_manager import BaseTokenManager

try:
from update_checker import update_check
Expand Down Expand Up @@ -172,6 +173,8 @@ def __init__(
config_interpolation: Optional[str] = None,
requestor_class: Optional[Type[Requestor]] = None,
requestor_kwargs: Dict[str, Any] = None,
*,
token_manager: Optional[BaseTokenManager] = None,
**config_settings: Union[str, bool],
): # noqa: D207, D301
"""Initialize a Reddit instance.
Expand All @@ -188,6 +191,10 @@ def __init__(
requestor. If not set, use ``asyncprawcore.Requestor`` (default: None).
:param requestor_kwargs: Dictionary with additional keyword arguments
used to initialize the requestor (default: None).
:param token_manager: When provided, the passed instance, a subclass of
:class:`.BaseTokenManager`, will manage tokens via two callback functions.
This parameter must be provided in order to work with refresh tokens
(default: None).
Additional keyword arguments will be used to initialize the
:class:`.Config` object. This can be used to specify configuration
Expand Down Expand Up @@ -249,6 +256,7 @@ async def request(self, *args, **kwargs):
"""
self._core = self._authorized_core = self._read_only_core = None
self._objector = None
self._token_manager = token_manager
self._unique_counter = 0
self._validate_on_submit = False

Expand Down Expand Up @@ -438,6 +446,29 @@ def _check_for_update(self):
update_check(__package__, __version__)
Reddit.update_checked = True

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:
raise TypeError(
"legacy ``refresh_token`` setting cannot be provided when providing"
" ``token_manager``"
)

self._token_manager.reddit = self
authorizer = Authorizer(
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:
authorizer = Authorizer(
authenticator, refresh_token=self.config._do_not_use_refresh_token
)
else:
self._core = self._read_only_core
return
self._core = self._authorized_core = session(authorizer)

def _prepare_objector(self):
mappings = {
self.config.kinds["comment"]: models.Comment,
Expand Down Expand Up @@ -513,23 +544,16 @@ def _prepare_trusted_asyncprawcore(self, requestor):
authenticator, self.config.username, self.config.password
)
self._core = self._authorized_core = session(script_authorizer)
elif self.config.refresh_token:
authorizer = Authorizer(authenticator, self.config.refresh_token)
self._core = self._authorized_core = session(authorizer)
else:
self._core = self._read_only_core
self._prepare_common_authorizer(authenticator)

def _prepare_untrusted_asyncprawcore(self, requestor):
authenticator = UntrustedAuthenticator(
requestor, self.config.client_id, self.config.redirect_uri
)
read_only_authorizer = DeviceIDAuthorizer(authenticator)
self._read_only_core = session(read_only_authorizer)
if self.config.refresh_token:
authorizer = Authorizer(authenticator, self.config.refresh_token)
self._core = self._authorized_core = session(authorizer)
else:
self._core = self._read_only_core
self._prepare_common_authorizer(authenticator)

async def comment(
self, # pylint: disable=invalid-name
Expand Down
83 changes: 83 additions & 0 deletions asyncpraw/util/token_manager.py
@@ -0,0 +1,83 @@
"""Token Manager classes.
There should be a 1-to-1 mapping between an instance of a subclass of
:class:`.BaseTokenManager` and a :class:`.Reddit` instance.
A few trivial 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.
"""
import aiofiles


class BaseTokenManager:
"""An abstract class for all token managers."""

def __init__(self):
"""Prepare attributes needed by all token manager classes."""
self._reddit = None

@property
def reddit(self):
"""Return the :class:`.Reddit` instance bound to the token manager."""
return self._reddit

@reddit.setter
def reddit(self, value):
if self._reddit is not None:
raise RuntimeError(
"``reddit`` can only be set once and is done automatically"
)
self._reddit = value

def post_refresh_callback(self, authorizer):
"""Handle callback that is invoked after a refresh token is used.
:param authorizer: The ``asyncprawcore.Authorizer`` instance used containing
``access_token`` and ``refresh_token`` attributes.
This function will be called after refreshing the access and refresh
tokens. This callback can be used for saving the updated
``refresh_token``.
"""
raise NotImplementedError("``post_refresh_callback`` must be extended.")

def pre_refresh_callback(self, authorizer):
"""Handle callback that is invoked before refreshing PRAW's authorization.
:param authorizer: The ``asyncprawcore.Authorizer`` instance used containing
``access_token`` and ``refresh_token`` attributes.
This callback can be used to inspect and modify the attributes of the
``asyncprawcore.Authorizer`` instance, such as setting the
``refresh_token``.
"""
raise NotImplementedError("``pre_refresh_callback`` must be extended.")


class FileTokenManager(BaseTokenManager):
"""Provides a trivial single-file based token manager."""

def __init__(self, filename):
"""Load and save refresh tokens from a file.
:param filename: The file the contains the refresh token.
"""
super().__init__()
self._filename = filename

async def post_refresh_callback(self, authorizer):
"""Update the saved copy of the refresh token."""
async with aiofiles.open(self._filename, "w") as fp:
await fp.write(authorizer.refresh_token)

async def pre_refresh_callback(self, authorizer):
"""Load the refresh token from the file."""
if authorizer.refresh_token is None:
async with aiofiles.open(self._filename) as fp:
authorizer.refresh_token = (await fp.read()).strip()
1 change: 1 addition & 0 deletions docs/code_overview/other.rst
Expand Up @@ -122,5 +122,6 @@ instances of them bound to an attribute of one of the Async PRAW models.
other/subredditremovalreasons
other/subredditrules
other/redditorstream
other/token_manager
other/trophy
other/util
5 changes: 5 additions & 0 deletions docs/code_overview/other/token_manager.rst
@@ -0,0 +1,5 @@
Token Manager
=============

.. automodule:: asyncpraw.util.token_manager
:inherited-members:
1 change: 1 addition & 0 deletions docs/examples/lmgtfy_bot.py 100644 → 100755
@@ -1,3 +1,4 @@
#!/usr/bin/env python3
import asyncio
from urllib.parse import quote_plus

Expand Down

0 comments on commit b455d23

Please sign in to comment.