Skip to content

Commit

Permalink
Merge pull request #1069 from Pyprohly/objector-cleanup-1
Browse files Browse the repository at this point in the history
Objector changes
  • Loading branch information
bboe committed May 21, 2019
2 parents be1dc77 + fd129f7 commit 9d8f633
Show file tree
Hide file tree
Showing 14 changed files with 93 additions and 61 deletions.
1 change: 1 addition & 0 deletions AUTHORS.rst
Expand Up @@ -3,6 +3,7 @@ Maintainers

- Bryce Boe <bbzbryce@gmail.com> `@bboe <https://github.com/bboe>`_
- Joe RH <jarhill0@gmail.com> `@jarhill0 <https://github.com/jarhill0>`_
- Daniel P. <pyprohly@outlook.com> `@Pyprohly <https://github.com/Pyprohly>`_


Documentation Contributors
Expand Down
5 changes: 4 additions & 1 deletion CHANGES.rst
Expand Up @@ -9,10 +9,13 @@ Unreleased
* Collections (:class:`.Collection` and helper classes).
* :meth:`.submit`, :meth:`.submit_image`, and :meth:`.submit_video` can be used
to submit a post directly to a collection.
* ``FullnameMixin.kind``
* ``praw.util.camel_to_snake`` and ``praw.util.snake_case_keys``.

**Changed**

* :meth:`.Reddit.info` now accepts any non-str iterable for fullnames.
* :meth:`.Reddit.info` now accepts any non-str iterable for fullnames
(not just ``list``).
* :meth:`.Reddit.info` now returns a generator instead of a list when
using the ``url`` parameter.

Expand Down
5 changes: 5 additions & 0 deletions praw/models/reddit/comment.py
Expand Up @@ -73,6 +73,11 @@ def id_from_url(url):
raise ClientException("Invalid URL: {}".format(url))
return parts[-1]

@property
def kind(self):
"""Return the class's kind."""
return self._reddit.config.kinds["comment"]

@property
def is_root(self):
"""Return True when the comment is a top level comment."""
Expand Down
1 change: 1 addition & 0 deletions praw/models/reddit/live.py
Expand Up @@ -553,6 +553,7 @@ class LiveUpdate(FullnameMixin, RedditBase):
"""

STR_FIELD = "id"
kind = "LiveUpdate"

@cachedproperty
def contrib(self):
Expand Down
5 changes: 5 additions & 0 deletions praw/models/reddit/message.py
Expand Up @@ -71,6 +71,11 @@ def parse(cls, data, reddit):

return cls(reddit, _data=data)

@property
def kind(self):
"""Return the class's kind."""
return self._reddit.config.kinds["message"]

def __init__(self, reddit, _data):
"""Construct an instance of the Message object."""
super(Message, self).__init__(reddit, _data=_data)
Expand Down
6 changes: 3 additions & 3 deletions praw/models/reddit/mixins/fullname.py
Expand Up @@ -6,6 +6,8 @@
class FullnameMixin(object):
"""Interface for classes that have a fullname."""

kind = None

@property
def fullname(self):
"""Return the object's fullname.
Expand All @@ -14,9 +16,7 @@ def fullname(self):
underscore and the object's base36 ID, e.g., ``t1_c5s96e0``.
"""
return "{}_{}".format(
self._reddit._objector.kind(self), self.id
) # pylint: disable=invalid-name
return "{}_{}".format(self.kind, self.id)

def _fetch(self):
if "_info_path" in dir(self):
Expand Down
3 changes: 2 additions & 1 deletion praw/models/reddit/modmail.py
@@ -1,5 +1,6 @@
"""Provide models for new modmail."""
from ...const import API_PATH
from ...util import snake_case_keys
from .base import RedditBase


Expand Down Expand Up @@ -118,7 +119,7 @@ def parse( # pylint: disable=arguments-differ
cls._convert_conversation_objects(data, reddit)
)

conversation = reddit._objector._snake_case_keys(conversation)
conversation = snake_case_keys(conversation)

return cls(reddit, _data=conversation)

Expand Down
5 changes: 5 additions & 0 deletions praw/models/reddit/redditor.py
Expand Up @@ -97,6 +97,11 @@ def stream(self):
"""
return RedditorStream(self)

@property
def kind(self):
"""Return the class's kind."""
return self._reddit.config.kinds["redditor"]

def __init__(self, reddit, name=None, _data=None):
"""Initialize a Redditor instance.
Expand Down
5 changes: 5 additions & 0 deletions praw/models/reddit/submission.py
Expand Up @@ -97,6 +97,11 @@ def id_from_url(url):
raise ClientException("Invalid URL: {}".format(url))
return submission_id

@property
def kind(self):
"""Return the class's kind."""
return self._reddit.config.kinds["submission"]

@property
def comments(self):
"""Provide an instance of :class:`.CommentForest`.
Expand Down
5 changes: 5 additions & 0 deletions praw/models/reddit/subreddit.py
Expand Up @@ -188,6 +188,11 @@ def _subreddit_list(subreddit, other_subreddits):
)
return str(subreddit)

@property
def kind(self):
"""Return the class's kind."""
return self._reddit.config.kinds["subreddit"]

@cachedproperty
def banned(self):
"""Provide an instance of :class:`.SubredditRelationship`.
Expand Down
60 changes: 8 additions & 52 deletions praw/objector.py
@@ -1,55 +1,20 @@
"""Provides the Objector class."""
import re

from .exceptions import APIException
from .util import snake_case_keys


class Objector(object):
"""The objector builds :class:`.RedditBase` objects."""

@staticmethod
def _camel_to_snake(name):
"""Return `name` converted from camelCase to snake_case.
Code from http://stackoverflow.com/a/1176023/.
"""
first_break_replaced = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
return re.sub(
"([a-z0-9])([A-Z])", r"\1_\2", first_break_replaced
).lower()

@classmethod
def _snake_case_keys(cls, dictionary):
"""Return a copy of dictionary with keys converted to snake_case.
:param dictionary: The dict to be corrected.
"""
return {cls._camel_to_snake(k): v for k, v in dictionary.items()}

def __init__(self, reddit):
def __init__(self, reddit, parsers=None):
"""Initialize an Objector instance.
:param reddit: An instance of :class:`~.Reddit`.
"""
self.parsers = {}
self.parsers = {} if parsers is None else parsers
self._reddit = reddit

def kind(self, instance):
"""Return the kind from the instance class.
:param instance: An instance of a subclass of RedditBase.
"""
retval = None
for key in self.parsers:
if isinstance(instance, self.parsers[key]):
retval = key
break
return retval

def _objectify_dict(self, data):
"""Create RedditBase objects from dicts.
Expand All @@ -61,27 +26,27 @@ def _objectify_dict(self, data):
parser = self.parsers["ModmailConversation"]
elif {"actionTypeId", "author", "date"}.issubset(data):
# Modmail mod action
data = self._snake_case_keys(data)
data = snake_case_keys(data)
parser = self.parsers["ModmailAction"]
elif {"bodyMarkdown", "isInternal"}.issubset(data):
# Modmail message
data = self._snake_case_keys(data)
data = snake_case_keys(data)
parser = self.parsers["ModmailMessage"]
elif {"isAdmin", "isDeleted"}.issubset(data):
# Modmail author
data = self._snake_case_keys(data)
data = snake_case_keys(data)
# Prevent clobbering base-36 id
del data["id"]
data["is_subreddit_mod"] = data.pop("is_mod")
parser = self.parsers[self._reddit.config.kinds["redditor"]]
elif {"banStatus", "muteStatus", "recentComments"}.issubset(data):
# Modmail user
data = self._snake_case_keys(data)
data = snake_case_keys(data)
data["created_string"] = data.pop("created")
parser = self.parsers[self._reddit.config.kinds["redditor"]]
elif {"displayName", "id", "type"}.issubset(data):
# Modmail subreddit
data = self._snake_case_keys(data)
data = snake_case_keys(data)
parser = self.parsers[self._reddit.config.kinds[data["type"]]]
elif {"date", "id", "name"}.issubset(data) or {
"id",
Expand Down Expand Up @@ -160,12 +125,3 @@ def objectify(self, data):
return self._objectify_dict(data)

return data

def register(self, kind, cls):
"""Register a class for a given kind.
:param kind: The kind in the parsed data to map to ``cls``.
:param cls: A RedditBase class.
"""
self.parsers[kind] = cls
4 changes: 1 addition & 3 deletions praw/reddit.py
Expand Up @@ -317,7 +317,6 @@ def _check_for_update(self):
Reddit.update_checked = True

def _prepare_objector(self):
self._objector = Objector(self)
mappings = {
self.config.kinds["comment"]: models.Comment,
self.config.kinds["message"]: models.Message,
Expand Down Expand Up @@ -355,8 +354,7 @@ def _prepare_objector(self):
"textarea": models.TextArea,
"widget": models.Widget,
}
for kind, klass in mappings.items():
self._objector.register(kind, klass)
self._objector = Objector(self, mappings)

def _prepare_prawcore(self, requestor_class=None, requestor_kwargs=None):
requestor_class = requestor_class or Requestor
Expand Down
20 changes: 19 additions & 1 deletion praw/util/__init__.py
@@ -1,3 +1,21 @@
"""Package imports for utilities."""

__all__ = ("cache",)
import re

__all__ = ("cache", "camel_to_snake", "snake_case_keys")

_re_camel_to_snake = re.compile(r"([a-z0-9](?=[A-Z])|[A-Z](?=[A-Z][a-z]))")


def camel_to_snake(name):
"""Convert `name` from camelCase to snake_case."""
return _re_camel_to_snake.sub(r"\1_", name).lower()


def snake_case_keys(dictionary):
"""Return a new dictionary with keys converted to snake_case.
:param dictionary: The dict to be corrected.
"""
return {camel_to_snake(k): v for k, v in dictionary.items()}
29 changes: 29 additions & 0 deletions tests/unit/util/test_util.py
@@ -0,0 +1,29 @@
from praw.util import camel_to_snake

from .. import UnitTest


class TestUtil(UnitTest):
def test_camel_to_snake(self):
test_strings = (
("camelCase", "camel_case"),
("PascalCase", "pascal_case"),
("camelCasePlace", "camel_case_place"),
("Pascal8Camel8Snake", "pascal8_camel8_snake"),
("HTTPResponseCode", "http_response_code"),
("ResponseHTTP", "response_http"),
("ResponseHTTP200", "response_http200"),
("getHTTPResponseCode", "get_http_response_code"),
("get200HTTPResponseCode", "get200_http_response_code"),
("getHTTP200ResponseCode", "get_http200_response_code"),
("12PolarBears", "12_polar_bears"),
("11buzzingBees", "11buzzing_bees"),
("TacocaT", "tacoca_t"),
("fooBARbaz", "foo_ba_rbaz"),
("foo_BAR_baz", "foo_bar_baz"),
("Base_BASE", "base_base"),
("Case_Case", "case_case"),
("FACE_Face", "face_face"),
)
for camel, snake in test_strings:
assert camel_to_snake(camel) == snake

0 comments on commit 9d8f633

Please sign in to comment.