Repository: falconry/falcon

Issue:
Unannounced breaking change in renaming falcon.api_helpers
The `falcon.api_helpers` module used to contain several semi-public (not documented, but not underscored either) utilities, that people may be relying on.

During the 3.0 release cycle, `falcon.api_helpers` was renamed to `falcon.app_helpers` in order to reflect the `API` :arrow_right: `App` renaming.
However, the old name disappeared without any deprecation notice, and without any information in the 3.0 changelog notes. This is a bug.

In the spirit of SemVer, we cannot afford marking the change as deprecation in 3.0.1 either.
Let us make the name available as-is in 3.0.1, and mark as deprecated in 3.1; remove in 4.0.


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/falconry/falcon/pull/1904?src=pr&el=h1) Report
> Merging [#1904](https://codecov.io/gh/falconry/falcon/pull/1904?src=pr&el=desc) (805c72c) into [master](https://codecov.io/gh/falconry/falcon/commit/f610ea42ef5733618cb0f5abbf7318d6463796a7?el=desc) (f610ea4) will **decrease** coverage by `63.94%`.
> The diff coverage is `50.00%`.

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

```diff
@@             Coverage Diff              @@
##            master    #1904       +/-   ##
============================================
- Coverage   100.00%   36.05%   -63.95%     
============================================
  Files           62       63        +1     
  Lines         6591     6593        +2     
  Branches      1065     1065               
============================================
- Hits          6591     2377     -4214     
- Misses           0     4163     +4163     
- Partials         0       53       +53     
```


| [Impacted Files](https://codecov.io/gh/falconry/falcon/pull/1904?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [falcon/api\_helpers.py](https://codecov.io/gh/falconry/falcon/pull/1904/diff?src=pr&el=tree#diff-ZmFsY29uL2FwaV9oZWxwZXJzLnB5) | `0.00% <0.00%> (ø)` | |
| [falcon/app\_helpers.py](https://codecov.io/gh/falconry/falcon/pull/1904/diff?src=pr&el=tree#diff-ZmFsY29uL2FwcF9oZWxwZXJzLnB5) | `47.25% <100.00%> (-52.75%)` | :arrow_down: |
| [falcon/util/reader.py](https://codecov.io/gh/falconry/falcon/pull/1904/diff?src=pr&el=tree#diff-ZmFsY29uL3V0aWwvcmVhZGVyLnB5) | `12.62% <0.00%> (-87.38%)` | :arrow_down: |
| [falcon/asgi/stream.py](https://codecov.io/gh/falconry/falcon/pull/1904/diff?src=pr&el=tree#diff-ZmFsY29uL2FzZ2kvc3RyZWFtLnB5) | `13.58% <0.00%> (-86.42%)` | :arrow_down: |
| [falcon/asgi/structures.py](https://codecov.io/gh/falconry/falcon/pull/1904/diff?src=pr&el=tree#diff-ZmFsY29uL2FzZ2kvc3RydWN0dXJlcy5weQ==) | `14.58% <0.00%> (-85.42%)` | :arrow_down: |
| [falcon/asgi/reader.py](https://codecov.io/gh/falconry/falcon/pull/1904/diff?src=pr&el=tree#diff-ZmFsY29uL2FzZ2kvcmVhZGVyLnB5) | `16.41% <0.00%> (-83.59%)` | :arrow_down: |
| [falcon/middleware.py](https://codecov.io/gh/falconry/falcon/pull/1904/diff?src=pr&el=tree#diff-ZmFsY29uL21pZGRsZXdhcmUucHk=) | `18.36% <0.00%> (-81.64%)` | :arrow_down: |
| [falcon/testing/helpers.py](https://codecov.io/gh/falconry/falcon/pull/1904/diff?src=pr&el=tree#diff-ZmFsY29uL3Rlc3RpbmcvaGVscGVycy5weQ==) | `19.63% <0.00%> (-80.37%)` | :arrow_down: |
| [falcon/asgi/app.py](https://codecov.io/gh/falconry/falcon/pull/1904/diff?src=pr&el=tree#diff-ZmFsY29uL2FzZ2kvYXBwLnB5) | `23.05% <0.00%> (-76.95%)` | :arrow_down: |
| [falcon/request.py](https://codecov.io/gh/falconry/falcon/pull/1904/diff?src=pr&el=tree#diff-ZmFsY29uL3JlcXVlc3QucHk=) | `23.44% <0.00%> (-76.56%)` | :arrow_down: |
| ... and [45 more](https://codecov.io/gh/falconry/falcon/pull/1904/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/falconry/falcon/pull/1904?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/1904?src=pr&el=footer). Last update [f610ea4...805c72c](https://codecov.io/gh/falconry/falcon/pull/1904?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

CaselIT: thanks, updated

Patch:
diff --git a/docs/_newsfragments/1902.rst b/docs/_newsfragments/1902.rst
new file mode 100644
index 0000000..2d0ebd8
--- /dev/null
+++ b/docs/_newsfragments/1902.rst
@@ -0,0 +1,2 @@
+Re-add ``api_helpers`` module that was renamed ``app_helpers``.
+This module is considered deprecated, and will be removed in a future Falcon version.
\ No newline at end of file
diff --git a/falcon/api_helpers.py b/falcon/api_helpers.py
new file mode 100644
index 0000000..9024872
--- /dev/null
+++ b/falcon/api_helpers.py
@@ -0,0 +1,7 @@
+from .app_helpers import * # NOQA
+
+# TODO deprecate
+# import warnings
+# from .util.deprecation import DeprecatedWarning
+
+# warnings.warn('The api_helpers module was renamed to app_helpers.', DeprecatedWarning)
diff --git a/falcon/app_helpers.py b/falcon/app_helpers.py
index 2d9bcd0..03951b7 100644
--- a/falcon/app_helpers.py
+++ b/falcon/app_helpers.py
@@ -22,6 +22,13 @@ from falcon.constants import MEDIA_XML
 from falcon.errors import CompatibilityError
 from falcon.util.sync import _wrap_non_coroutine_unsafe
 
+__all__ = (
+    'prepare_middleware',
+    'prepare_middleware_ws',
+    'default_serialize_error',
+    'CloseableStreamIterator'
+)
+
 
 def prepare_middleware(middleware, independent_middleware=False, asgi=False):
     """Check middleware interfaces and prepare the methods for request handling.


Test patch:
diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py
index 09570a3..dd6a033 100644
--- a/tests/test_deprecations.py
+++ b/tests/test_deprecations.py
@@ -1,6 +1,30 @@
-from falcon import request_helpers, stream
+import pytest
+
+from falcon import app_helpers, request_helpers, stream
+
+# from _util import has_cython
 
 
 def test_bounded_stream():
     assert request_helpers.Body is stream.Body
     assert request_helpers.BoundedStream is stream.BoundedStream
+
+
+class TestApiHelpers:
+    @pytest.mark.filterwarnings('ignore:The api_helpers')
+    def test_imports(self):
+        from falcon import api_helpers
+
+        for name in app_helpers.__all__:
+            assert getattr(api_helpers, name) is getattr(app_helpers, name)
+
+    # TODO enable test of deprecation
+    # @pytest.mark.skipif(has_cython, reason='Reloading modules on Cython does not work')
+    # def test_warning(self):
+    #     import importlib
+
+    #     from falcon.util.deprecation import DeprecatedWarning
+
+    #     with pytest.warns(DeprecatedWarning, match='The api_helpers'):
+    #         from falcon import api_helpers
+    #         importlib.reload(api_helpers)