Repository: ucfopen/canvasapi

Issue:
File.download hangs in debug statement indefinately
# Describe the bug

File.download hangs when downloading a ~230 MB tgz file. I waited 10+ minutes, but never long enough to see it terminate. I have not tried enough files to produce a minimal working example.

Based on traceback when cancelling mid-operation, the library appears hung in this try/except block:
```
            try:
                logger.debug("Data: {data}".format(data=pformat(response.json())))
            except ValueError:
                logger.debug("Data: {data}".format(data=pformat(response.text)))
```

Specifically, its hung performing `response.json()`
```
  ...
  File "/Users/gondree/anaconda/envs/hob/lib/python3.6/site-packages/canvasapi/file.py", line 32, in download
    response = self._requester.request("GET", _url=self.url)
  File "/Users/gondree/anaconda/envs/hob/lib/python3.6/site-packages/canvasapi/requester.py", line 226, in request
    logger.debug("Data: {data}".format(data=pformat(response.json())))
  File "/Users/gondree/anaconda/envs/hob/lib/python3.6/site-packages/requests/models.py", line 898, in json
    return complexjson.loads(self.text, **kwargs)
  File "/Users/gondree/anaconda/envs/hob/lib/python3.6/site-packages/requests/models.py", line 858, in text
    encoding = self.apparent_encoding
  File "/Users/gondree/anaconda/envs/hob/lib/python3.6/site-packages/requests/models.py", line 728, in apparent_encoding
    return chardet.detect(self.content)['encoding']
  File "/Users/gondree/anaconda/envs/hob/lib/python3.6/site-packages/chardet/__init__.py", line 38, in detect
    detector.feed(byte_str)
  File "/Users/gondree/anaconda/envs/hob/lib/python3.6/site-packages/chardet/universaldetector.py", line 211, in feed
    if prober.feed(byte_str) == ProbingState.FOUND_IT:
  File "/Users/gondree/anaconda/envs/hob/lib/python3.6/site-packages/chardet/charsetgroupprober.py", line 71, in feed
    state = prober.feed(byte_str)
  File "/Users/gondree/anaconda/envs/hob/lib/python3.6/site-packages/chardet/sbcharsetprober.py", line 79, in feed
    byte_str = self.filter_international_words(byte_str)
  File "/Users/gondree/anaconda/envs/hob/lib/python3.6/site-packages/chardet/charsetprober.py", line 87, in filter_international_words
    buf)
  File "/Users/gondree/anaconda/envs/hob/lib/python3.6/re.py", line 222, in findall
    return _compile(pattern, flags).findall(string)
```

# Expected behavior

The expected behavior is that the file gets downloaded, no matter the size and no matter the contents.

# Environment information

- Python version (`python --version`)
```
$ python --version
Python 3.6.2 :: Continuum Analytics, Inc.
```

- CanvasAPI version (`pip show canvasapi`)
```
Name: canvasapi
Version: 0.16.0
Summary: API wrapper for the Canvas LMS
Home-page: https://github.com/ucfopen/canvasapi
Author: University of Central Florida - Center for Distributed Learning
Author-email: techrangers@ucf.edu
License: MIT License
Location: /Users/gondree/anaconda/envs/hob/lib/python3.6/site-packages
Requires: requests, pytz, six
Required-by: 
```
# Additional context

I tried to implement a timeout, like:
```
        ohandler = signal.getsignal(signal.SIGALRM)
        try:
            signal.signal(signal.SIGALRM, lambda a,b: (_ for _ in ()).throw(ValueError))
            signal.alarm(2)
            logger.debug("Data: {data}".format(data=pformat(response.json())))
        except ValueError:
            logger.debug("Data: {data}".format(data=pformat(response.text)))
        finally:
            signal.signal(signal.SIGALRM, ohandler)
            signal.alarm(0)
```
But, generally, that did not work correctly. 

The simplest fix for me was avoiding the operations when not debugging:
```
        if logger.isEnabledFor(logging.DEBUG):
            try:
                logger.debug("Data: {data}".format(data=pformat(response.json())))
            except ValueError:
                logger.debug("Data: {data}".format(data=pformat(response.text)))
```
This solved my problem. It would not be ideal if you ever needed to debug in a situation involving one of these problematic downloads. Reporting in hope that this, or a more clever fix, can be incorporated.

Discussion:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/35068988/badge)](https://coveralls.io/builds/35068988)

Coverage remained the same at 100.0% when pulling **8b3300b9b0acd5f674bdd2fc4d544f4029858653 on blepabyte:issue/386-encoding-fixes** into **463174ed5339acf069830e848b08ed29d8797e56 on ucfopen:develop**.

Thetwam: Great find and solution. Thanks for the help @blepabyte !

Travis is being super slow, but it passed in py 3.6 there and on 3.8 on my local. Am going to override merge.

Patch:
diff --git a/canvasapi/file.py b/canvasapi/file.py
index ea68a52..81439f6 100644
--- a/canvasapi/file.py
+++ b/canvasapi/file.py
@@ -32,11 +32,15 @@ class File(CanvasObject):
         with open(location, "wb") as file_out:
             file_out.write(response.content)
 
-    def get_contents(self):
+    def get_contents(self, binary=False):
         """
         Download the contents of this file.
+        Pass binary=True to return a bytes object instead of a str.
 
-        :rtype: str
+        :rtype: str or bytes
         """
         response = self._requester.request("GET", _url=self.url)
-        return response.text
+        if binary:
+            return response.content
+        else:
+            return response.text
diff --git a/canvasapi/requester.py b/canvasapi/requester.py
index fbdfee3..2d236ce 100644
--- a/canvasapi/requester.py
+++ b/canvasapi/requester.py
@@ -156,7 +156,7 @@ class Requester(object):
             currently only the POST request of GraphQL is using this parameter.
             For all other methods it's just passed and ignored.
         :type json: `bool`
-        :rtype: str
+        :rtype: :class:`requests.Response`
         """
         full_url = _url if _url else "{}{}".format(self.base_url, endpoint)
 
@@ -217,9 +217,14 @@ class Requester(object):
         )
 
         try:
-            logger.debug("Data: {data}".format(data=pformat(response.json())))
-        except ValueError:
-            logger.debug("Data: {data}".format(data=pformat(response.text)))
+            logger.debug(
+                "Data: {data}".format(data=pformat(response.content.decode("utf-8")))
+            )
+        except UnicodeDecodeError:
+            logger.debug("Data: {data}".format(data=pformat(response.content)))
+        except AttributeError:
+            # response.content is None
+            logger.debug("No data")
 
         # Add response to internal cache
         if len(self._cache) > 4:


Test patch:
diff --git a/tests/test_file.py b/tests/test_file.py
index 97048a5..ed7a569 100644
--- a/tests/test_file.py
+++ b/tests/test_file.py
@@ -53,3 +53,5 @@ class TestFile(unittest.TestCase):
         register_uris({"file": ["file_contents"]}, m)
         contents = self.file.get_contents()
         self.assertEqual(contents, '"Hello there"')
+        contents_binary = self.file.get_contents(binary=True)
+        self.assertEqual(contents_binary, b'"Hello there"')
diff --git a/tests/test_requester.py b/tests/test_requester.py
index cf85bc8..df59eb9 100644
--- a/tests/test_requester.py
+++ b/tests/test_requester.py
@@ -32,6 +32,18 @@ class TestRequester(unittest.TestCase):
         response = self.requester.request("GET", "fake_get_request")
         self.assertEqual(response.status_code, 200)
 
+    def test_request_get_binary(self, m):
+        m.register_uri(
+            "GET",
+            settings.BASE_URL_WITH_VERSION + "get_binary_data",
+            content=b"\xff\xff\xff",
+            status_code=200,
+            headers={},
+        )
+
+        response = self.requester.request("GET", "get_binary_data")
+        self.assertEqual(response.content, b"\xff\xff\xff")
+
     def test_request_get_datetime(self, m):
         date = datetime.today()