Skip to content

Commit cae50da

Browse files
myuskovytas7
andauthored
fix(multipart): don't share MultipartParseOptions._DEFAULT_HANDLERS (#2322)
* fix(multipart): don't share MultipartParseOptions._DEFAULT_HANDLERS between instances * fix(media): implement a proper `Handlers.copy()` method * chore: add a versionadded docs directive * docs(newsfragments): add a newsfragment for #2293 --------- Co-authored-by: Vytautas Liuolia <vytautas.liuolia@gmail.com>
1 parent 1f914c5 commit cae50da

File tree

4 files changed

+42
-1
lines changed

4 files changed

+42
-1
lines changed

docs/_newsfragments/2293.bugfix.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Customizing
2+
:attr:`MultipartParseOptions.media_handlers
3+
<falcon.media.multipart.MultipartParseOptions.media_handlers>` could previously
4+
lead to unintentionally modifying a shared class variable.
5+
This has been fixed, and the
6+
:attr:`~falcon.media.multipart.MultipartParseOptions.media_handlers` attribute
7+
is now initialized to a fresh copy of handlers for every instance of
8+
:class:`~falcon.media.multipart.MultipartParseOptions`. To that end, a proper
9+
:meth:`~falcon.media.Handlers.copy` method has been implemented for the media
10+
:class:`~falcon.media.Handlers` class.

falcon/media/handlers.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,22 @@ def resolve(
164164

165165
return cast(ResolverMethod, resolve)
166166

167+
def copy(self) -> Handlers:
168+
"""Create a shallow copy of this instance of handlers.
169+
170+
The resulting copy contains the same keys and values, but it can be
171+
customized separately without affecting the original object.
172+
173+
Returns:
174+
A shallow copy of handlers.
175+
176+
.. versionadded:: 4.0
177+
"""
178+
# NOTE(vytas): In the unlikely case we are dealing with a subclass,
179+
# return the matching type.
180+
handlers_cls = type(self)
181+
return handlers_cls(self.data)
182+
167183
@deprecation.deprecated(
168184
'This undocumented method is no longer supported as part of the public '
169185
'interface and will be removed in a future release.'

falcon/media/multipart.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,4 +610,7 @@ def __init__(self) -> None:
610610
self.max_body_part_buffer_size = 1024 * 1024
611611
self.max_body_part_count = 64
612612
self.max_body_part_headers_size = 8192
613-
self.media_handlers = self._DEFAULT_HANDLERS
613+
# NOTE(myusko,vytas): Here we create a copy of _DEFAULT_HANDLERS in
614+
# order to prevent the modification of the class variable whenever
615+
# parse_options.media_handlers are customized.
616+
self.media_handlers = self._DEFAULT_HANDLERS.copy()

tests/test_media_multipart.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import falcon
99
from falcon import media
1010
from falcon import testing
11+
from falcon.media.multipart import MultipartParseOptions
1112
from falcon.util import BufferedReader
1213

1314
try:
@@ -849,3 +850,14 @@ async def deserialize_async(self, stream, content_type, content_length):
849850

850851
assert resp.status_code == 200
851852
assert resp.json == ['', '0x48']
853+
854+
855+
def test_multipart_parse_options_default_handlers_unique():
856+
parse_options_one = MultipartParseOptions()
857+
parse_options_two = MultipartParseOptions()
858+
859+
parse_options_one.media_handlers.pop(falcon.MEDIA_JSON)
860+
861+
assert parse_options_one.media_handlers is not parse_options_two.media_handlers
862+
assert len(parse_options_one.media_handlers) == 1
863+
assert len(parse_options_two.media_handlers) >= 2

0 commit comments

Comments
 (0)