Repository: praw-dev/praw

Issue:
[Suggestion] Get redditor object by user_id
## Issue Description

It would be nice to get redditor object with the user id. 
If possible.



Discussion:
Does Reddit's API permit that? If not then it's not something PRAW can do.
> 
> 
> Does Reddit's API permit that? If not then it's not something PRAW can do.

I found some old reddit posts saying its doable, also in praw there is a function to get user object by username.
Can you link said posts please? I don't recall off hand such way to convert from user id to username. Out of curiosity, why do you have user ids?
> Can you link said posts please? I don't recall off hand such way to convert from user id to username. Out of curiosity, why do you have user ids?

[Post Link](https://www.reddit.com/r/redditdev/comments/ac0w3l/how_do_i_get_a_redditor_instance_by_redditor_id/) - [Suggested Solution](https://www.reddit.com/r/redditdev/comments/ac0w3l/how_do_i_get_a_redditor_instance_by_redditor_id/ed4hdeh?utm_source=share&utm_medium=web2x) - [Solution Tested and Working](https://www.reddit.com/r/redditdev/comments/ac0w3l/how_do_i_get_a_redditor_instance_by_redditor_id/ed4jj0g?utm_source=share&utm_medium=web2x)

Because the database i was using at that time was accessible to others and i didn't have a log for the database changes, i used user_ids so no data would be changed by the lower tier admins. I currently switched to `author.name` but still, the feature might be usable for later time, for other people.

Resolving patch:
diff --git a/praw/endpoints.py b/praw/endpoints.py
--- a/praw/endpoints.py
+++ b/praw/endpoints.py
@@ -184,6 +184,7 @@
     "upload_image":            "r/{subreddit}/api/upload_sr_img",
     "user":                    "user/{user}/",
     "user_about":              "user/{user}/about/",
+    "user_by_fullname":        "/api/user_data_by_account_ids",
     "user_flair":              "r/{subreddit}/api/user_flair_v2",
     "users_new":               "users/new",
     "users_popular":           "users/popular",
diff --git a/praw/models/reddit/redditor.py b/praw/models/reddit/redditor.py
--- a/praw/models/reddit/redditor.py
+++ b/praw/models/reddit/redditor.py
@@ -102,15 +102,25 @@ def _kind(self):
         """Return the class's kind."""
         return self._reddit.config.kinds["redditor"]
 
-    def __init__(self, reddit, name=None, _data=None):
+    @property
+    def _path(self):
+        return API_PATH["user"].format(user=self)
+
+    def __init__(self, reddit, name=None, fullname=None, _data=None):
         """Initialize a Redditor instance.
 
         :param reddit: An instance of :class:`~.Reddit`.
         :param name: The name of the redditor.
+        :param fullname: The fullname of the redditor, starting with ``t2_``.
+
+        Exactly one of ``name``, ``fullname`` or ``_data`` must be provided.
 
         """
-        if bool(name) == bool(_data):
-            raise TypeError("Either `name` or `_data` must be provided.")
+        if [name, fullname, _data].count(None) != 2:
+            raise TypeError(
+                "Exactly one of `name`, `fullname`, or `_data` must be "
+                "provided."
+            )
         if _data:
             assert (
                 isinstance(_data, dict) and "name" in _data
@@ -119,9 +129,17 @@ def __init__(self, reddit, name=None, _data=None):
         self._listing_use_sort = True
         if name:
             self.name = name
-        self._path = API_PATH["user"].format(user=self)
+        elif fullname:
+            self._fullname = fullname
+
+    def _fetch_username(self, fullname):
+        return self._reddit.get(
+            API_PATH["user_by_fullname"], params={"ids": fullname}
+        )[fullname]["name"]
 
     def _fetch_info(self):
+        if hasattr(self, "_fullname"):
+            self.name = self._fetch_username(self._fullname)
         return ("user_about", {"user": self.name}, None)
 
     def _fetch_data(self):
diff --git a/praw/reddit.py b/praw/reddit.py
--- a/praw/reddit.py
+++ b/praw/reddit.py
@@ -556,13 +556,16 @@ def random_subreddit(self, nsfw=False):
             path = redirect.path
         return models.Subreddit(self, path.split("/")[2])
 
-    def redditor(self, name):
-        """Return a lazy instance of :class:`~.Redditor` for ``name``.
+    def redditor(self, name=None, fullname=None):
+        """Return a lazy instance of :class:`~.Redditor`.
 
         :param name: The name of the redditor.
+        :param fullname: The fullname of the redditor, starting with ``t2_``.
+
+        Either ``name`` or ``fullname`` can be provided, but not both.
 
         """
-        return models.Redditor(self, name)
+        return models.Redditor(self, name=name, fullname=fullname)
 
     def request(self, method, path, params=None, data=None, files=None):
         """Return the parsed JSON data returned from a request to URL.


Test patch:
diff --git a/tests/integration/cassettes/TestRedditor.test_fullname_init.json b/tests/integration/cassettes/TestRedditor.test_fullname_init.json
new file mode 100644
--- /dev/null
+++ b/tests/integration/cassettes/TestRedditor.test_fullname_init.json
@@ -0,0 +1,337 @@
+{
+  "http_interactions": [
+    {
+      "recorded_at": "2019-11-20T05:35:08",
+      "request": {
+        "body": {
+          "encoding": "utf-8",
+          "string": "grant_type=password&password=<PASSWORD>&username=<USERNAME>"
+        },
+        "headers": {
+          "Accept": [
+            "*/*"
+          ],
+          "Accept-Encoding": [
+            "identity"
+          ],
+          "Authorization": [
+            "Basic <BASIC_AUTH>"
+          ],
+          "Connection": [
+            "keep-alive"
+          ],
+          "Content-Length": [
+            "64"
+          ],
+          "Content-Type": [
+            "application/x-www-form-urlencoded"
+          ],
+          "User-Agent": [
+            "<USER_AGENT> PRAW/6.3.2.dev0 prawcore/1.0.1"
+          ]
+        },
+        "method": "POST",
+        "uri": "https://www.reddit.com/api/v1/access_token"
+      },
+      "response": {
+        "body": {
+          "encoding": "UTF-8",
+          "string": "{\"access_token\": \"<ACCESS_TOKEN>\", \"token_type\": \"bearer\", \"expires_in\": 3600, \"scope\": \"*\"}"
+        },
+        "headers": {
+          "Accept-Ranges": [
+            "bytes"
+          ],
+          "Connection": [
+            "keep-alive"
+          ],
+          "Content-Length": [
+            "114"
+          ],
+          "Content-Type": [
+            "application/json; charset=UTF-8"
+          ],
+          "Date": [
+            "Wed, 20 Nov 2019 05:35:08 GMT"
+          ],
+          "Server": [
+            "snooserv"
+          ],
+          "Set-Cookie": [
+            "edgebucket=3ZzV21j44YwZ7b1Ub4; Domain=reddit.com; Max-Age=63071999; Path=/;  secure"
+          ],
+          "Strict-Transport-Security": [
+            "max-age=15552000; includeSubDomains; preload"
+          ],
+          "Via": [
+            "1.1 varnish"
+          ],
+          "X-Cache": [
+            "MISS"
+          ],
+          "X-Cache-Hits": [
+            "0"
+          ],
+          "X-Moose": [
+            "majestic"
+          ],
+          "X-Served-By": [
+            "cache-akl1420-AKL"
+          ],
+          "X-Timer": [
+            "S1574228107.368283,VS0,VE759"
+          ],
+          "cache-control": [
+            "max-age=0, must-revalidate"
+          ],
+          "x-content-type-options": [
+            "nosniff"
+          ],
+          "x-frame-options": [
+            "SAMEORIGIN"
+          ],
+          "x-xss-protection": [
+            "1; mode=block"
+          ]
+        },
+        "status": {
+          "code": 200,
+          "message": "OK"
+        },
+        "url": "https://www.reddit.com/api/v1/access_token"
+      }
+    },
+    {
+      "recorded_at": "2019-11-20T05:35:09",
+      "request": {
+        "body": {
+          "encoding": "utf-8",
+          "string": ""
+        },
+        "headers": {
+          "Accept": [
+            "*/*"
+          ],
+          "Accept-Encoding": [
+            "identity"
+          ],
+          "Authorization": [
+            "bearer <ACCESS_TOKEN>"
+          ],
+          "Connection": [
+            "keep-alive"
+          ],
+          "Cookie": [
+            "edgebucket=3ZzV21j44YwZ7b1Ub4"
+          ],
+          "User-Agent": [
+            "<USER_AGENT> PRAW/6.3.2.dev0 prawcore/1.0.1"
+          ]
+        },
+        "method": "GET",
+        "uri": "https://oauth.reddit.com/api/user_data_by_account_ids?ids=t2_6c1xj&raw_json=1"
+      },
+      "response": {
+        "body": {
+          "encoding": "UTF-8",
+          "string": "{\"t2_6c1xj\": {\"name\": \"PyAPITestUser3\", \"created_utc\": 1322552953.0, \"link_karma\": 0, \"comment_karma\": 0, \"profile_img\": \"https://www.redditstatic.com/avatars/avatar_default_12_FFB000.png\", \"profile_color\": \"\", \"profile_over_18\": false}}"
+        },
+        "headers": {
+          "Accept-Ranges": [
+            "bytes"
+          ],
+          "Connection": [
+            "keep-alive"
+          ],
+          "Content-Length": [
+            "237"
+          ],
+          "Content-Type": [
+            "application/json; charset=UTF-8"
+          ],
+          "Date": [
+            "Wed, 20 Nov 2019 05:35:09 GMT"
+          ],
+          "Server": [
+            "snooserv"
+          ],
+          "Strict-Transport-Security": [
+            "max-age=15552000; includeSubDomains; preload"
+          ],
+          "Via": [
+            "1.1 varnish"
+          ],
+          "X-Cache": [
+            "MISS"
+          ],
+          "X-Cache-Hits": [
+            "0"
+          ],
+          "X-Moose": [
+            "majestic"
+          ],
+          "X-Served-By": [
+            "cache-akl1420-AKL"
+          ],
+          "X-Timer": [
+            "S1574228108.266664,VS0,VE909"
+          ],
+          "cache-control": [
+            "private, s-maxage=0, max-age=0, must-revalidate, no-store, max-age=0, must-revalidate"
+          ],
+          "expires": [
+            "-1"
+          ],
+          "set-cookie": [
+            "loid=00000000000009zy7l.2.1356454469555.Z0FBQUFBQmQxTkNONjBRVVozNnNtb3RGaFloLXl1VXFUOTJiamFVbVI5R2JSYjl6TGg0VHh2VUtzanI5MmZDbWY5RGZScEo3REdXZkVkSC1ha2VyQ0YwTXVxTDA2aWRVaEpwWFdhVlNPMGhFUUxRLWZFeG4yT2dHR0JXazN4QTltUnBfNWpILXozMnU; Domain=reddit.com; Max-Age=63071999; Path=/; expires=Fri, 19-Nov-2021 05:35:09 GMT; secure",
+            "redesign_optout=true; Domain=reddit.com; Max-Age=94607999; Path=/; expires=Sat, 19-Nov-2022 05:35:09 GMT; secure",
+            "session_tracker=19VAGTtV82juLGRSNC.0.1574228109044.Z0FBQUFBQmQxTkNOTzU0Z0loMWN2WVhUY2VsUVo4b29LZHFRdDlSS00tck9IOGxtblhhUHRGSzZobEZ1R1pQQlRKQVFSZEtaWU1UUjJQWHpHMHh5Tm1wQjRXZ0x0bTNDem5XQTNKckZUXzMtSmozTXpIV2wwUEUwVEIwNkpUSGNIbWhLeUxBbmRZMUc; Domain=reddit.com; Max-Age=7199; Path=/; expires=Wed, 20-Nov-2019 07:35:09 GMT; secure"
+          ],
+          "x-content-type-options": [
+            "nosniff"
+          ],
+          "x-frame-options": [
+            "SAMEORIGIN"
+          ],
+          "x-ratelimit-remaining": [
+            "594.0"
+          ],
+          "x-ratelimit-reset": [
+            "291"
+          ],
+          "x-ratelimit-used": [
+            "6"
+          ],
+          "x-ua-compatible": [
+            "IE=edge"
+          ],
+          "x-xss-protection": [
+            "1; mode=block"
+          ]
+        },
+        "status": {
+          "code": 200,
+          "message": "OK"
+        },
+        "url": "https://oauth.reddit.com/api/user_data_by_account_ids?ids=t2_6c1xj&raw_json=1"
+      }
+    },
+    {
+      "recorded_at": "2019-11-20T05:35:09",
+      "request": {
+        "body": {
+          "encoding": "utf-8",
+          "string": ""
+        },
+        "headers": {
+          "Accept": [
+            "*/*"
+          ],
+          "Accept-Encoding": [
+            "identity"
+          ],
+          "Authorization": [
+            "bearer <ACCESS_TOKEN>"
+          ],
+          "Connection": [
+            "keep-alive"
+          ],
+          "Cookie": [
+            "edgebucket=3ZzV21j44YwZ7b1Ub4; loid=00000000000009zy7l.2.1356454469555.Z0FBQUFBQmQxTkNONjBRVVozNnNtb3RGaFloLXl1VXFUOTJiamFVbVI5R2JSYjl6TGg0VHh2VUtzanI5MmZDbWY5RGZScEo3REdXZkVkSC1ha2VyQ0YwTXVxTDA2aWRVaEpwWFdhVlNPMGhFUUxRLWZFeG4yT2dHR0JXazN4QTltUnBfNWpILXozMnU; redesign_optout=true; session_tracker=19VAGTtV82juLGRSNC.0.1574228109044.Z0FBQUFBQmQxTkNOTzU0Z0loMWN2WVhUY2VsUVo4b29LZHFRdDlSS00tck9IOGxtblhhUHRGSzZobEZ1R1pQQlRKQVFSZEtaWU1UUjJQWHpHMHh5Tm1wQjRXZ0x0bTNDem5XQTNKckZUXzMtSmozTXpIV2wwUEUwVEIwNkpUSGNIbWhLeUxBbmRZMUc"
+          ],
+          "User-Agent": [
+            "<USER_AGENT> PRAW/6.3.2.dev0 prawcore/1.0.1"
+          ]
+        },
+        "method": "GET",
+        "uri": "https://oauth.reddit.com/user/PyAPITestUser3/about/?raw_json=1"
+      },
+      "response": {
+        "body": {
+          "encoding": "UTF-8",
+          "string": "{\"kind\": \"t2\", \"data\": {\"is_employee\": false, \"icon_img\": \"https://www.redditstatic.com/avatars/avatar_default_12_FFB000.png\", \"pref_show_snoovatar\": false, \"name\": \"PyAPITestUser3\", \"is_friend\": false, \"created\": 1322581753.0, \"has_subscribed\": true, \"hide_from_robots\": false, \"created_utc\": 1322552953.0, \"link_karma\": 1, \"comment_karma\": 0, \"is_gold\": false, \"is_mod\": false, \"verified\": true, \"subreddit\": {\"default_set\": true, \"user_is_contributor\": false, \"banner_img\": \"\", \"restrict_posting\": true, \"user_is_banned\": false, \"free_form_reports\": true, \"community_icon\": \"\", \"show_media\": true, \"icon_color\": \"#FFB000\", \"user_is_muted\": false, \"display_name\": \"u_PyAPITestUser3\", \"header_img\": null, \"title\": \"\", \"over_18\": false, \"icon_size\": [256, 256], \"primary_color\": \"\", \"icon_img\": \"https://www.redditstatic.com/avatars/avatar_default_12_FFB000.png\", \"description\": \"\", \"submit_link_label\": \"\", \"header_size\": null, \"restrict_commenting\": false, \"subscribers\": 0, \"submit_text_label\": \"\", \"is_default_icon\": true, \"link_flair_position\": \"\", \"display_name_prefixed\": \"u/PyAPITestUser3\", \"key_color\": \"\", \"name\": \"t5_b43nd\", \"is_default_banner\": true, \"url\": \"/user/PyAPITestUser3/\", \"banner_size\": null, \"user_is_moderator\": false, \"public_description\": \"\", \"link_flair_enabled\": false, \"disable_contributor_requests\": false, \"subreddit_type\": \"user\", \"user_is_subscriber\": false}, \"has_verified_email\": true, \"id\": \"6c1xj\"}}"
+        },
+        "headers": {
+          "Accept-Ranges": [
+            "bytes"
+          ],
+          "Connection": [
+            "keep-alive"
+          ],
+          "Content-Length": [
+            "1437"
+          ],
+          "Content-Type": [
+            "application/json; charset=UTF-8"
+          ],
+          "Date": [
+            "Wed, 20 Nov 2019 05:35:09 GMT"
+          ],
+          "Server": [
+            "snooserv"
+          ],
+          "Strict-Transport-Security": [
+            "max-age=15552000; includeSubDomains; preload"
+          ],
+          "Vary": [
+            "accept-encoding"
+          ],
+          "Via": [
+            "1.1 varnish"
+          ],
+          "X-Cache": [
+            "MISS"
+          ],
+          "X-Cache-Hits": [
+            "0"
+          ],
+          "X-Moose": [
+            "majestic"
+          ],
+          "X-Served-By": [
+            "cache-akl1420-AKL"
+          ],
+          "X-Timer": [
+            "S1574228109.238158,VS0,VE298"
+          ],
+          "cache-control": [
+            "private, s-maxage=0, max-age=0, must-revalidate, no-store, max-age=0, must-revalidate"
+          ],
+          "expires": [
+            "-1"
+          ],
+          "set-cookie": [
+            "session_tracker=19VAGTtV82juLGRSNC.0.1574228109353.Z0FBQUFBQmQxTkNOV2hIYlRDS1FscFhoa0RuVFJrS3Z6RnRrTnBjZlVuWVdCby1JZkZ6UEUzUlppYzU2WG1QZ1ZaZW5WS3BmZGp0V19UY0FHNndaUC00QTFYanpwbERCaWtyNkdHaTBXQTNTRHJvY2UtcHJLeHhvbDJzV0U0aGZrTVFxQ1p1Mk50VEg; Domain=reddit.com; Max-Age=7199; Path=/; expires=Wed, 20-Nov-2019 07:35:09 GMT; secure"
+          ],
+          "x-content-type-options": [
+            "nosniff"
+          ],
+          "x-frame-options": [
+            "SAMEORIGIN"
+          ],
+          "x-ratelimit-remaining": [
+            "593.0"
+          ],
+          "x-ratelimit-reset": [
+            "291"
+          ],
+          "x-ratelimit-used": [
+            "7"
+          ],
+          "x-ua-compatible": [
+            "IE=edge"
+          ],
+          "x-xss-protection": [
+            "1; mode=block"
+          ]
+        },
+        "status": {
+          "code": 200,
+          "message": "OK"
+        },
+        "url": "https://oauth.reddit.com/user/PyAPITestUser3/about/?raw_json=1"
+      }
+    }
+  ],
+  "recorded_with": "betamax/0.8.1"
+}
\ No newline at end of file
diff --git a/tests/integration/models/reddit/test_redditor.py b/tests/integration/models/reddit/test_redditor.py
--- a/tests/integration/models/reddit/test_redditor.py
+++ b/tests/integration/models/reddit/test_redditor.py
@@ -9,6 +9,7 @@
 
 class TestRedditor(IntegrationTest):
     FRIEND = "PyAPITestUser3"
+    FRIEND_FULLNAME = "t2_6c1xj"
 
     @mock.patch("time.sleep", return_value=None)
     def test_block(self, _):
@@ -41,6 +42,13 @@ def test_friend_info(self, _):
             assert "created_utc" not in redditor.__dict__
             assert hasattr(redditor, "created_utc")
 
+    @mock.patch("time.sleep", return_value=None)
+    def test_fullname_init(self, _):
+        self.reddit.read_only = False
+        with self.recorder.use_cassette("TestRedditor.test_fullname_init"):
+            redditor = self.reddit.redditor(fullname=self.FRIEND_FULLNAME)
+            assert redditor.name == self.FRIEND
+
     def test_gild__no_creddits(self):
         self.reddit.read_only = False
         with self.recorder.use_cassette("TestRedditor.test_gild__no_creddits"):
diff --git a/tests/unit/models/reddit/test_redditor.py b/tests/unit/models/reddit/test_redditor.py
--- a/tests/unit/models/reddit/test_redditor.py
+++ b/tests/unit/models/reddit/test_redditor.py
@@ -21,13 +21,28 @@ def test_equality(self):
         assert redditor2 == "dummy1"
 
     def test_construct_failure(self):
-        message = "Either `name` or `_data` must be provided."
+        message = (
+            "Exactly one of `name`, `fullname`, or `_data` must be provided."
+        )
         with pytest.raises(TypeError) as excinfo:
             Redditor(self.reddit)
         assert str(excinfo.value) == message
 
         with pytest.raises(TypeError) as excinfo:
-            Redditor(self.reddit, "dummy", {"id": "dummy"})
+            Redditor(self.reddit, "dummy", _data={"id": "dummy"})
+        assert str(excinfo.value) == message
+
+        with pytest.raises(TypeError) as excinfo:
+            Redditor(self.reddit, name="dummy", fullname="t2_dummy")
+        assert str(excinfo.value) == message
+
+        with pytest.raises(TypeError) as excinfo:
+            Redditor(
+                self.reddit,
+                name="dummy",
+                fullname="t2_dummy",
+                _data={"id": "dummy"},
+            )
         assert str(excinfo.value) == message
 
         with pytest.raises(AssertionError):