Repository: pallets/werkzeug

Issue:
url_for appends ? if called with empty query argument
### Expected Behavior
If `url_for` is called with a query argument that is an empty list (and no other query arguments), the URL should have no query part at all, including no trailing question mark. (When called with a nonempty list, it adds several query arguments with the same name, which is what I expect; when called with `None`, it omits the question mark already.)

```python
@app.route('/')
def index():
    return flask.url_for('index', extra_param=[])
```
Full example: https://github.com/lucaswerkmeister/repro-flask-url_for-empty

### Actual Behavior
The URL has a question mark at the end (in the above example: `/?`).

### Environment

* Python version: 3.9.1
* Flask version: 1.1.2
* Werkzeug version: 1.0.1

### Workaround

Replace the empty list with `None` by appending `or None` to the argument. (This also works in Jinja2 templates.)


Discussion:
The version with the question mark is valid. ~I can't remember the exact reason, but this has been discussed before. If you are in a situation where you'll be sending potentially empty arguments and don't want it, you can write a wrapper to strip it off.~
Went back to 0.14 and this didn't happen there. `git bisect` points to #1281, which introduced the new code generator for URL building.

Resolving patch:
diff --git a/src/werkzeug/routing.py b/src/werkzeug/routing.py
--- a/src/werkzeug/routing.py
+++ b/src/werkzeug/routing.py
@@ -2239,7 +2239,7 @@ def build(
         :param url_scheme: Scheme to use in place of the bound
             :attr:`url_scheme`.
 
-        .. versionadded:: 2.0
+        .. versionchanged:: 2.0
             Added the ``url_scheme`` parameter.
 
         .. versionadded:: 0.6
@@ -2248,22 +2248,29 @@ def build(
         self.map.update()
 
         if values:
-            if isinstance(values, MultiDict):
-                temp_values = {}
-                # dict.items(values) is like `values.lists()`
-                # without the call or `list()` coercion overhead.
-                for key, value in dict.items(values):
+            temp_values: t.Dict[str, t.Union[t.List[t.Any], t.Any]] = {}
+            always_list = isinstance(values, MultiDict)
+            key: str
+            value: t.Optional[t.Union[t.List[t.Any], t.Any]]
+
+            # For MultiDict, dict.items(values) is like values.lists()
+            # without the call or list coercion overhead.
+            for key, value in dict.items(values):  # type: ignore
+                if value is None:
+                    continue
+
+                if always_list or isinstance(value, (list, tuple)):
+                    value = [v for v in value if v is not None]
+
                     if not value:
                         continue
-                    if len(value) == 1:  # flatten single item lists
+
+                    if len(value) == 1:
                         value = value[0]
-                        if value is None:  # drop None
-                            continue
-                    temp_values[key] = value
-                values = temp_values
-            else:
-                # drop None
-                values = dict(i for i in values.items() if i[1] is not None)
+
+                temp_values[key] = value
+
+            values = temp_values
         else:
             values = {}
 


Test patch:
diff --git a/tests/test_routing.py b/tests/test_routing.py
--- a/tests/test_routing.py
+++ b/tests/test_routing.py
@@ -711,16 +711,30 @@ def test_anyconverter():
     assert a.match("/a.2") == ("yes_dot", {"a": "a.2"})
 
 
-def test_build_append_unknown():
-    map = r.Map([r.Rule("/bar/<float:bazf>", endpoint="barf")])
-    adapter = map.bind("example.org", "/", subdomain="blah")
+@pytest.mark.parametrize(
+    ("value", "expect"),
+    [
+        (None, ""),
+        ([None], ""),
+        ([None, None], ""),
+        ("", "?extra="),
+        ([""], "?extra="),
+        (1.0, "?extra=1.0"),
+        ([1, 2], "?extra=1&extra=2"),
+        ([1, None, 2], "?extra=1&extra=2"),
+        ([1, "", 2], "?extra=1&extra=&extra=2"),
+    ],
+)
+def test_build_append_unknown(value, expect):
+    map = r.Map([r.Rule("/foo/<name>", endpoint="foo")])
+    adapter = map.bind("example.org", "/", subdomain="test")
     assert (
-        adapter.build("barf", {"bazf": 0.815, "bif": 1.0})
-        == "http://example.org/bar/0.815?bif=1.0"
+        adapter.build("foo", {"name": "bar", "extra": value})
+        == f"http://example.org/foo/bar{expect}"
     )
     assert (
-        adapter.build("barf", {"bazf": 0.815, "bif": 1.0}, append_unknown=False)
-        == "http://example.org/bar/0.815"
+        adapter.build("foo", {"name": "bar", "extra": value}, append_unknown=False)
+        == "http://example.org/foo/bar"
     )