Repository: pallets/werkzeug (python)
Issue: HTTPException.description can no longer be None in werkzeug 2.x.
<!--
This issue tracker is a tool to address bugs in Werkzeug itself. Please
use Pallets Discord or Stack Overflow for questions about your own code.

Replace this comment with a clear outline of what the bug is.
-->

The werkzeug HTTPException class has a `description` property, which is allowed to be `None` according to type hints.

In werkzeug 1.x, this worked fine. In werkzeug 2.x, having `description` set to None leads to an exception:

```python
Python 3.7.5 (v3.7.5:5c02a39a0b, Oct 14 2019, 18:49:57) 
[Clang 6.0 (clang-600.0.57)] on darwin
from werkzeug.exceptions import HTTPException
e = HTTPException()
e.get_description()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "/Users/d073668/work/ml/meh/ML-MEH-Commons/venv/lib/python3.7/site-packages/werkzeug/exceptions.py", line 158, in get_description
    description = escape(self.description).replace("\n", "<br>")  # type: ignore
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/html/__init__.py", line 19, in escape
    s = s.replace("&", "&amp;") # Must be done first!
AttributeError: 'NoneType' object has no attribute 'replace'
```

I believe this error is happening because the `escape` function used in `HTTPException.get_description` has been replaced. In Werkzeug 1.x, this was `werkzeug.utils.escape` which replaces `None` with the empty string`""`. In werkzeug 2.x, this is now coming from Python's built-in `html` package, which does not handle `None`.

Environment:

- Python version: 3.7.5
- Werkzeug version: 2.0.0 (broken)

Pull request description: convert `HTTPException.description` to string
Allows `HTTPDescription` to be `None` again. Additionally allows it to be another object like a dict, which seems to a pattern that is used to pass structured information to turn into a JSON response. I'm not sure that's actually documented as supported, so I'm not changing the type annotations for now.

- fixes #2115 

<!--
Ensure each step in CONTRIBUTING.rst is complete by adding an "x" to
each box below.

If only docs were changed, these aren't relevant and can be removed.
-->

Checklist:

- [x] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
- [x] Add or update relevant docs, in the docs folder and in code.
- [x] Add an entry in `CHANGES.rst` summarizing the change and linking to the issue.
- [x] Add `.. versionchanged::` entries in any relevant code docs.
- [x] Run `pre-commit` hooks and fix any issues.
- [x] Run `pytest` and `tox`, no tests failed.


Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 8a8af6e9..7444a201 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -16,6 +16,8 @@ Unreleased
     characters. :issue:`2125`
 -   Type checking understands that calling ``headers.get`` with a string
     default will always return a string. :issue:`2128`
+-   If ``HTTPException.description`` is not a string,
+    ``get_description`` will convert it to a string. :issue:`2115`
 
 
 Version 2.0.0
diff --git a/src/werkzeug/exceptions.py b/src/werkzeug/exceptions.py
index 7c43d85e..a3e3b079 100644
--- a/src/werkzeug/exceptions.py
+++ b/src/werkzeug/exceptions.py
@@ -156,7 +156,14 @@ class HTTPException(Exception):
         scope: t.Optional[dict] = None,
     ) -> str:
         """Get the description."""
-        description = escape(self.description).replace("\n", "<br>")  # type: ignore
+        if self.description is None:
+            description = ""
+        elif not isinstance(self.description, str):
+            description = str(self.description)
+        else:
+            description = self.description
+
+        description = escape(description).replace("\n", "<br>")
         return f"<p>{description}</p>"
 
     def get_body(


Test patch:
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
index 12ea8b92..2a683905 100644
--- a/tests/test_exceptions.py
+++ b/tests/test_exceptions.py
@@ -142,3 +142,7 @@ def test_passing_response(cls):
     exc = cls(response=TestResponse())
     rp = exc.get_response({})
     assert isinstance(rp, TestResponse)
+
+
+def test_description_none():
+    HTTPException().get_response()