Skip to content

feat(request): new method get_param_as_dict#2544

Open
StepanUFL wants to merge 7 commits intofalconry:masterfrom
StepanUFL:feature/request-git-param-as-dict
Open

feat(request): new method get_param_as_dict#2544
StepanUFL wants to merge 7 commits intofalconry:masterfrom
StepanUFL:feature/request-git-param-as-dict

Conversation

@StepanUFL
Copy link

@StepanUFL StepanUFL commented Oct 7, 2025

Add a new method get_param_as_dict in order to handle OAS "objects"

Closes issue #2542

Summary of Changes

A new method req.get_param_as_dict() has added to the Request class in falcon/util/request.py

Related Issues

Closes #2542.

Pull Request Checklist

This is just a reminder about the most common mistakes. Please make sure that you tick all appropriate boxes. Reading our contribution guide at least once will save you a few review cycles!

If an item doesn't apply to your pull request, check it anyway to make it apparent that there's nothing to do.

  • Applied changes to both WSGI and ASGI code paths and interfaces (where applicable).
  • Added tests for changed code.
  • Performed automated tests and code quality checks by running tox.
  • Prefixed code comments with GitHub nick and an appropriate prefix.
  • Coding style is consistent with the rest of the framework.
  • Updated documentation for changed code.
    • Added docstrings for any new classes, functions, or modules.
    • Updated docstrings for any modifications to existing code.
    • Updated both WSGI and ASGI docs (where applicable).
    • Added references to new classes, functions, or modules to the relevant RST file under docs/.
    • Updated all relevant supporting documentation files under docs/.
    • A copyright notice is included at the top of any new modules (using your own name or the name of your organization).
    • Changed/added classes/methods/functions have appropriate versionadded, versionchanged, or deprecated directives.
  • Changes (and possible deprecations) have towncrier news fragments under docs/_newsfragments/, with the file name format {issue_number}.{fragment_type}.rst. (Run towncrier --draft to ensure it renders correctly.)

If you have any questions to any of the points above, just submit and ask! This checklist is here to help you, not to deter you from contributing!

PR template inspired by the attrs project.

Hope this is worthy enough for you all! If there's improvements to be made, let me know

StepanUFL and others added 2 commits October 7, 2025 05:35
Add a new method get_param_as_dict in order to handle OAS "objects"

Closes issue falconry#2542
@codecov
Copy link

codecov bot commented Oct 7, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (002fa6c) to head (bae1d32).

Additional details and impacted files
@@            Coverage Diff            @@
##            master     #2544   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           64        64           
  Lines         7911      7945   +34     
  Branches      1086      1096   +10     
=========================================
+ Hits          7911      7945   +34     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Member

@CaselIT CaselIT left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the contribution, I've left a few suggestions

deep_object: bool = False,
store: StoreArg = None,
default: Any | None = None,
) -> Any:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is likely dict[str, str | None] | None even if we don't want to do the overload like the other ones

if deep_object:
oc: dict[str, Any] = {}
for key, value in self._params.items():
if not key.startswith(f'{name}[') and key.endswith(']'):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we could optimize this a bit by caching the format, but it's likely not that important

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another thing to try would be to benchmark this flow vs using a regex, but I doubt a regex would be faster.

inner = key[len(name) + 1 : -1]

if isinstance(value, (list, tuple)):
oc[inner] = value[0] if value else None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if we want to use '' instead of None here. depending on it we may want to update the returned type here https://github.com/falconry/falcon/pull/2544/files#r2411775142

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: does this case ever happen (empty list)?

Copy link
Author

@StepanUFL StepanUFL Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably not in actual requests... left check in to be safe but can take out

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from my point of view we could keep this check just to be paranoid, but I would use '' in the else case, so that we can just day that the returned values are strings in all cases

@@ -0,0 +1,118 @@
from __future__ import annotations
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we already have a file for these test, called tests/test_query_params.py. could you move the tests there?

also you can probably check there how those methods are tested

output = dict(zip(values_list[::2], values_list[1::2]))

if store is not None and isinstance(output, dict):
store.update(output)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should do like in the other methods

if store is not None:
  store[name] = output



def test_deep_object_with_list_values():
req = DummyRequestParams({'user[name]': ['Bond'], 'user[id]': ['007']})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also test an with a list of multiple objects

and if we decide that an empty list can indeed happen, also an empty list

StepanUFL and others added 3 commits October 7, 2025 16:41
Co-authored-by: Federico Caselli <cfederico87@gmail.com>
Co-authored-by: Federico Caselli <cfederico87@gmail.com>
Copy link
Member

@vytas7 vytas7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this PR!
The base structure looks good, although I had a handful of nitpicks wrt the implementation (see inline).

HTTPBadRequest: A required param is missing from the request, or
the value could not be parsed from the parameter.

.. versionadded:: 4.1.0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be the current development version, Falcon 4.2.0, not 4.1.0 (which is already released).


else:
try:
values_list = self.get_param_as_list(name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a TODO comment here to also support delimiter from #2538?

values_list = self.get_param_as_list(name)
except errors.HTTPBadRequest:
msg = 'It could not parse the query parameter'
raise errors.HTTPInvalidParam(msg, name) from None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does raising from None have any special purpose here?
Add a code comment explaining the reasoning if there is any clever trick behind this.

try:
values_list = self.get_param_as_list(name)
except errors.HTTPBadRequest:
msg = 'It could not parse the query parameter'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This "It" here sounds slightly odd, could we try to make it more similar to other error messages?

if values_list is None:
if required:
msg = 'Missing query parameter'
raise errors.HTTPMissingParam(msg)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HTTPMissingParam takes the name of the missing parameter as its first argument, not an error message.


else:
if len(values_list) % 2 != 0:
msg = 'Invalid parameter format, list elements must be even'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be precise list elements cannot be even per se unless they are integer numbers. Suggested rewording: "...the number of list elements must be even".



# NOTE(StepanUFL): If a better way to make this play well with mypy exists...
DummyRequestParams.get_param_as_dict = Request.get_param_as_dict # type: ignore[method-assign, assignment]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defining a new fake DummyRequestParams is a somewhat unorthodox way to approach this 🤔.

Maybe we could simulate request, or simply construct a normal req object via create_req() (and create_asgi_req())?

To that end, we also have a util fixture (see tests/conftest.py), which can already construct both WSGI and ASGI requests, see util.create_req(asgi, ...).

@vytas7
Copy link
Member

vytas7 commented Mar 8, 2026

Hi again @StepanUFL, just checking if you are still considering to finish this PR?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add new method req.get_param_as_dict()

3 participants