Repository: falconry/falcon
Pull request #1058 (2017-05-23 00:11:39)

feat: Make it easier to set content-type
This is a follow up PR addressing the changes in PR: #965 

ofek: @mayurmahajan @jmvrbanac It would be much nicer if content types were an enum.
mayurmahajan: Could you please provide an example on how enum will be more helpful here?
ofek: Following the Zen of Python, "Explicit is better than implicit." Those constant names could be anything; the user doesn't know by the name itself. Also, imports look nicer:

```python
from falcon import ContentTypes
```

vs

```python
from falcon import JSON, HTML, JPEG, PNG
```
jmvrbanac: @ofek Unfortunately, Enums aren't currently supported in 2.6 and 2.7. While we can add a dependency, I'm not sure it worth it at the moment. Currently, our current constants are like `HTTP_XXX` so it would make sense to follow that pattern for the moment. So, to your point, I agree that we should be a little more explicit with the names. As a result, I think we should rename these to `MEDIA_XXXX` and then I've created an issue (#1059) to investigate refactoring our usage of constants. 
codecov[bot]: # [Codecov](https://codecov.io/gh/falconry/falcon/pull/1058?src=pr&el=h1) Report
> Merging [#1058](https://codecov.io/gh/falconry/falcon/pull/1058?src=pr&el=desc) into [master](https://codecov.io/gh/falconry/falcon/commit/f7849a8ac53b825c783b9ea726c6e9a6acc427c9?src=pr&el=desc) will **decrease** coverage by `0.09%`.
> The diff coverage is `83.33%`.

[![Impacted file tree graph](https://codecov.io/gh/falconry/falcon/pull/1058/graphs/tree.svg?token=zOtkfNQ3h6&width=650&height=150&src=pr)](https://codecov.io/gh/falconry/falcon/pull/1058?src=pr&el=tree)

```diff
@@           Coverage Diff            @@
##           master   #1058     +/-   ##
========================================
- Coverage     100%   99.9%   -0.1%     
========================================
  Files          30      31      +1     
  Lines        2169    2181     +12     
  Branches      339     340      +1     
========================================
+ Hits         2169    2179     +10     
- Misses          0       2      +2
```


| [Impacted Files](https://codecov.io/gh/falconry/falcon/pull/1058?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [falcon/constants/media\_types.py](https://codecov.io/gh/falconry/falcon/pull/1058?src=pr&el=tree#diff-ZmFsY29uL2NvbnN0YW50cy9tZWRpYV90eXBlcy5weQ==) | `100% <100%> (ø)` | |
| [falcon/response.py](https://codecov.io/gh/falconry/falcon/pull/1058?src=pr&el=tree#diff-ZmFsY29uL3Jlc3BvbnNlLnB5) | `98.64% <33.33%> (-1.36%)` | :arrow_down: |
| [falcon/bench/queues/api.py](https://codecov.io/gh/falconry/falcon/pull/1058?src=pr&el=tree#diff-ZmFsY29uL2JlbmNoL3F1ZXVlcy9hcGkucHk=) | `100% <0%> (ø)` | :arrow_up: |
| [falcon/bench/nuts/nuts/controllers/\_\_init\_\_.py](https://codecov.io/gh/falconry/falcon/pull/1058?src=pr&el=tree#diff-ZmFsY29uL2JlbmNoL251dHMvbnV0cy9jb250cm9sbGVycy9fX2luaXRfXy5weQ==) | `100% <0%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/falconry/falcon/pull/1058?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/falconry/falcon/pull/1058?src=pr&el=footer). Last update [f7849a8...1d5b1a7](https://codecov.io/gh/falconry/falcon/pull/1058?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

kgriffs: Thanks!

Patch:
diff --git a/falcon/__init__.py b/falcon/__init__.py
index b9176bb..8a8aafe 100644
--- a/falcon/__init__.py
+++ b/falcon/__init__.py
@@ -36,12 +36,10 @@ HTTP_METHODS = (
     'TRACE',
 )
 
-DEFAULT_MEDIA_TYPE = 'application/json; charset=UTF-8'
-
-
 # Hoist classes and functions into the falcon namespace
 from falcon.version import __version__  # NOQA
-from falcon.api import API, DEFAULT_MEDIA_TYPE  # NOQA
+from falcon.constants.media_types import *  # NOQA
+from falcon.api import API  # NOQA
 from falcon.status_codes import *  # NOQA
 from falcon.errors import *  # NOQA
 from falcon.redirects import *  # NOQA
diff --git a/falcon/api.py b/falcon/api.py
index 7da0783..96d06b8 100644
--- a/falcon/api.py
+++ b/falcon/api.py
@@ -36,6 +36,7 @@ class API(object):
     Args:
         media_type (str, optional): Default media type to use as the value for
             the Content-Type header on responses (default 'application/json').
+            It is possible to use default types like falcon.MEDIA_YAML, falcon.MEDIA_XML etc.
         middleware(object or list, optional): One or more objects
             (instantiated classes) that implement the following middleware
             component interface::
diff --git a/falcon/constants/__init__.py b/falcon/constants/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/falcon/constants/media_types.py b/falcon/constants/media_types.py
new file mode 100644
index 0000000..2a37dfb
--- /dev/null
+++ b/falcon/constants/media_types.py
@@ -0,0 +1,11 @@
+MEDIA_JSON = 'application/json; charset=UTF-8'
+MEDIA_HTML = 'text/html; charset=utf-8'
+MEDIA_JS = 'text/javascript; charset=utf-8'
+MEDIA_XML = 'application/xml'
+MEDIA_TEXT = 'text/plain; charset=utf-8'
+MEDIA_JPEG = 'image/jpeg'
+MEDIA_PNG = 'image/png'
+MEDIA_YAML = 'application/yaml'
+MEDIA_MSGPACK = 'application/msgpack'
+
+DEFAULT_MEDIA_TYPE = MEDIA_JSON
diff --git a/falcon/response.py b/falcon/response.py
index ce06dee..36fc919 100644
--- a/falcon/response.py
+++ b/falcon/response.py
@@ -642,7 +642,14 @@ class Response(object):
 
     content_type = header_property(
         'Content-Type',
-        'Set the Content-Type header.')
+        """Sets the Content-Type header.
+
+        Note:
+            You can use the following predefined content types: ``falcon.MEDIA_JSON``,
+            ``falcon.MEDIA_HTML``, ``falcon.MEDIA_JS``, ``falcon.MEDIA_XML``,
+            ``falcon.MEDIA_TEXT``, ``falcon.MEDIA_JPEG``, ``falcon.MEDIA_PNG``,
+            ``falcon.MEDIA_YAML`` and ``MEDIA_MSGPACK``
+        """)
 
     etag = header_property(
         'ETag',
@@ -713,24 +720,34 @@ class Response(object):
 
         """)
 
-    def _wsgi_headers(self, media_type=None, py2=PY2):
-        """Convert headers into the format expected by WSGI servers.
+    def _set_media_type(self, media_type=None):
+        """Wrapper around set_header to set a content-type.
 
         Args:
-            media_type: Default media type to use for the Content-Type
-                header if the header was not set explicitly (default ``None``).
+            media_type: Media type to use for the Content-Type
+                header.
 
         """
 
-        headers = self._headers
-
         # PERF(kgriffs): Using "in" like this is faster than using
         # dict.setdefault (tested on py27).
         set_content_type = (media_type is not None and
-                            'content-type' not in headers)
+                            'content-type' not in self._headers)
 
         if set_content_type:
-            headers['content-type'] = media_type
+            self.set_header('content-type', media_type)
+
+    def _wsgi_headers(self, media_type=None, py2=PY2):
+        """Convert headers into the format expected by WSGI servers.
+
+        Args:
+            media_type: Default media type to use for the Content-Type
+                header if the header was not set explicitly (default ``None``).
+
+        """
+
+        headers = self._headers
+        self._set_media_type(media_type)
 
         if py2:
             # PERF(kgriffs): Don't create an extra list object if


Test patch:
diff --git a/tests/test_response.py b/tests/test_response.py
new file mode 100644
index 0000000..333798c
--- /dev/null
+++ b/tests/test_response.py
@@ -0,0 +1,13 @@
+import falcon
+from falcon import MEDIA_TEXT
+
+
+def test_response_set_content_type_set():
+    resp = falcon.Response()
+    resp._set_media_type(MEDIA_TEXT)
+    assert resp._headers['content-type'] == MEDIA_TEXT
+
+
+def test_response_set_content_type_not_set():
+    resp = falcon.Response()
+    assert 'content-type' not in resp._headers