Skip to content

Commit

Permalink
fix(multipart): don't share MultipartParseOptions._DEFAULT_HANDLERS (#…
Browse files Browse the repository at this point in the history
…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 <[email protected]>
  • Loading branch information
myusko and vytas7 authored Sep 6, 2024
1 parent 1f914c5 commit cae50da
Show file tree
Hide file tree
Showing 4 changed files with 42 additions and 1 deletion.
10 changes: 10 additions & 0 deletions docs/_newsfragments/2293.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Customizing
:attr:`MultipartParseOptions.media_handlers
<falcon.media.multipart.MultipartParseOptions.media_handlers>` could previously
lead to unintentionally modifying a shared class variable.
This has been fixed, and the
:attr:`~falcon.media.multipart.MultipartParseOptions.media_handlers` attribute
is now initialized to a fresh copy of handlers for every instance of
:class:`~falcon.media.multipart.MultipartParseOptions`. To that end, a proper
:meth:`~falcon.media.Handlers.copy` method has been implemented for the media
:class:`~falcon.media.Handlers` class.
16 changes: 16 additions & 0 deletions falcon/media/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,22 @@ def resolve(

return cast(ResolverMethod, resolve)

def copy(self) -> Handlers:
"""Create a shallow copy of this instance of handlers.
The resulting copy contains the same keys and values, but it can be
customized separately without affecting the original object.
Returns:
A shallow copy of handlers.
.. versionadded:: 4.0
"""
# NOTE(vytas): In the unlikely case we are dealing with a subclass,
# return the matching type.
handlers_cls = type(self)
return handlers_cls(self.data)

@deprecation.deprecated(
'This undocumented method is no longer supported as part of the public '
'interface and will be removed in a future release.'
Expand Down
5 changes: 4 additions & 1 deletion falcon/media/multipart.py
Original file line number Diff line number Diff line change
Expand Up @@ -610,4 +610,7 @@ def __init__(self) -> None:
self.max_body_part_buffer_size = 1024 * 1024
self.max_body_part_count = 64
self.max_body_part_headers_size = 8192
self.media_handlers = self._DEFAULT_HANDLERS
# NOTE(myusko,vytas): Here we create a copy of _DEFAULT_HANDLERS in
# order to prevent the modification of the class variable whenever
# parse_options.media_handlers are customized.
self.media_handlers = self._DEFAULT_HANDLERS.copy()
12 changes: 12 additions & 0 deletions tests/test_media_multipart.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import falcon
from falcon import media
from falcon import testing
from falcon.media.multipart import MultipartParseOptions
from falcon.util import BufferedReader

try:
Expand Down Expand Up @@ -849,3 +850,14 @@ async def deserialize_async(self, stream, content_type, content_length):

assert resp.status_code == 200
assert resp.json == ['', '0x48']


def test_multipart_parse_options_default_handlers_unique():
parse_options_one = MultipartParseOptions()
parse_options_two = MultipartParseOptions()

parse_options_one.media_handlers.pop(falcon.MEDIA_JSON)

assert parse_options_one.media_handlers is not parse_options_two.media_handlers
assert len(parse_options_one.media_handlers) == 1
assert len(parse_options_two.media_handlers) >= 2

0 comments on commit cae50da

Please sign in to comment.