Repository: falconry/falcon
Pull request #1343 (2018-08-04 18:01:20)

Add headers property to Response class
Closes https://github.com/falconry/falcon/issues/1269

kgriffs: I added a few more commits per my most recent comments.
alysivji: Apologies. Was going to get to this over the weekend. Thanks for taking care of it!
kgriffs: @alysivji No worries, thanks for all your work on this!

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 6d06714..128b21a 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -23,6 +23,8 @@ Changes to Supported Platforms
 New & Improved
 --------------
 
+- Added a new ``headers`` property to the ``Response`` class.
+
 Fixed
 -----
 
diff --git a/docs/changes/2.0.0.rst b/docs/changes/2.0.0.rst
index 43e4698..71e8617 100644
--- a/docs/changes/2.0.0.rst
+++ b/docs/changes/2.0.0.rst
@@ -4,7 +4,7 @@ Changelog for Falcon 2.0.0
 Breaking Changes
 ----------------
 
-- :attr:`falcon.testing.Result.json` now returns None when the response body is
+- :attr:`falcon.testing.Result.json` now returns ``None`` when the response body is
   empty, rather than raising an error.
 - :meth:`~.Request.get_param_as_bool` now defaults to treating valueless
   parameters as truthy, rather than falsy. ``None`` is still returned
@@ -23,5 +23,7 @@ Changes to Supported Platforms
 New & Improved
 --------------
 
+- Added a new :attr:`~.Response.headers` property to the :class:`~.Response` class.
+
 Fixed
 -----
diff --git a/falcon/response.py b/falcon/response.py
index bde3c9b..14127a1 100644
--- a/falcon/response.py
+++ b/falcon/response.py
@@ -132,6 +132,10 @@ class Response(object):
                 the Response instance itself (self).
 
         options (dict): Set of global options passed from the API handler.
+
+        headers (dict): Copy of all headers set for the response,
+            sans cookies. Note that a new copy is created and returned each
+            time this property is referenced.
     """
 
     __slots__ = (
@@ -202,6 +206,10 @@ class Response(object):
     def data(self, value):
         self._data = value
 
+    @property
+    def headers(self):
+        return self._headers.copy()
+
     @property
     def media(self):
         return self._media


Test patch:
diff --git a/tests/test_response.py b/tests/test_response.py
index 333798c..1c88f35 100644
--- a/tests/test_response.py
+++ b/tests/test_response.py
@@ -1,3 +1,5 @@
+import pytest
+
 import falcon
 from falcon import MEDIA_TEXT
 
@@ -11,3 +13,36 @@ def test_response_set_content_type_set():
 def test_response_set_content_type_not_set():
     resp = falcon.Response()
     assert 'content-type' not in resp._headers
+
+
+def test_response_get_headers():
+    resp = falcon.Response()
+    resp.append_header('x-things1', 'thing-1')
+    resp.append_header('x-things2', 'thing-2')
+    resp.append_header('X-Things3', 'Thing-3')
+
+    resp.set_cookie('Chocolate', 'Chip')
+
+    headers = resp.headers
+    assert headers['x-things1'] == 'thing-1'
+    assert headers['x-things2'] == 'thing-2'
+    assert headers['x-things3'] == 'Thing-3'
+
+    assert 'set-cookie' not in headers
+
+
+def test_response_attempt_to_set_read_only_headers():
+    resp = falcon.Response()
+
+    resp.append_header('x-things1', 'thing-1')
+    resp.append_header('x-things2', 'thing-2')
+    resp.append_header('x-things3', 'thing-3a')
+    resp.append_header('X-Things3', 'thing-3b')
+
+    with pytest.raises(AttributeError):
+        resp.headers = {'x-things4': 'thing-4'}
+
+    headers = resp.headers
+    assert headers['x-things1'] == 'thing-1'
+    assert headers['x-things2'] == 'thing-2'
+    assert headers['x-things3'] == 'thing-3a, thing-3b'