6
6
import urllib .request
7
7
from collections .abc import MutableMapping
8
8
from http .cookiejar import Cookie , CookieJar
9
- from urllib .parse import parse_qsl , quote , unquote , urlencode
9
+ from urllib .parse import parse_qs , quote , unquote , urlencode
10
10
11
11
import idna
12
12
import rfc3986
48
48
URLTypes ,
49
49
)
50
50
from ._utils import (
51
- flatten_queryparams ,
52
51
guess_json_utf ,
53
52
is_known_encoding ,
54
53
normalize_header_key ,
@@ -148,8 +147,7 @@ def __init__(
148
147
# Add any query parameters, merging with any in the URL if needed.
149
148
if params :
150
149
if self ._uri_reference .query :
151
- url_params = QueryParams (self ._uri_reference .query )
152
- url_params .update (params )
150
+ url_params = QueryParams (self ._uri_reference .query ).merge (params )
153
151
query_string = str (url_params )
154
152
else :
155
153
query_string = str (QueryParams (params ))
@@ -450,7 +448,7 @@ def join(self, url: URLTypes) -> "URL":
450
448
451
449
url = httpx.URL("https://www.example.com/test")
452
450
url = url.join("/new/path")
453
- assert url == "https://www.example.com/test/ new/path"
451
+ assert url == "https://www.example.com/new/path"
454
452
"""
455
453
if self .is_relative_url :
456
454
# Workaround to handle relative URLs, which otherwise raise
@@ -504,38 +502,79 @@ def __init__(self, *args: QueryParamTypes, **kwargs: typing.Any) -> None:
504
502
items : typing .Sequence [typing .Tuple [str , PrimitiveData ]]
505
503
if value is None or isinstance (value , (str , bytes )):
506
504
value = value .decode ("ascii" ) if isinstance (value , bytes ) else value
507
- items = parse_qsl (value )
505
+ self . _dict = parse_qs (value )
508
506
elif isinstance (value , QueryParams ):
509
- items = value .multi_items ()
510
- elif isinstance (value , (list , tuple )):
511
- items = value
507
+ self ._dict = {k : list (v ) for k , v in value ._dict .items ()}
512
508
else :
513
- items = flatten_queryparams (value )
514
-
515
- self ._dict : typing .Dict [str , typing .List [str ]] = {}
516
- for item in items :
517
- k , v = item
518
- if str (k ) not in self ._dict :
519
- self ._dict [str (k )] = [primitive_value_to_str (v )]
509
+ dict_value : typing .Dict [typing .Any , typing .List [typing .Any ]] = {}
510
+ if isinstance (value , (list , tuple )):
511
+ # Convert list inputs like:
512
+ # [("a", "123"), ("a", "456"), ("b", "789")]
513
+ # To a dict representation, like:
514
+ # {"a": ["123", "456"], "b": ["789"]}
515
+ for item in value :
516
+ dict_value .setdefault (item [0 ], []).append (item [1 ])
520
517
else :
521
- self ._dict [str (k )].append (primitive_value_to_str (v ))
518
+ # Convert dict inputs like:
519
+ # {"a": "123", "b": ["456", "789"]}
520
+ # To dict inputs where values are always lists, like:
521
+ # {"a": ["123"], "b": ["456", "789"]}
522
+ dict_value = {
523
+ k : list (v ) if isinstance (v , (list , tuple )) else [v ]
524
+ for k , v in value .items ()
525
+ }
526
+
527
+ # Ensure that keys and values are neatly coerced to strings.
528
+ # We coerce values `True` and `False` to JSON-like "true" and "false"
529
+ # representations, and coerce `None` values to the empty string.
530
+ self ._dict = {
531
+ str (k ): [primitive_value_to_str (item ) for item in v ]
532
+ for k , v in dict_value .items ()
533
+ }
522
534
523
535
def keys (self ) -> typing .KeysView :
536
+ """
537
+ Return all the keys in the query params.
538
+
539
+ Usage:
540
+
541
+ q = httpx.QueryParams("a=123&a=456&b=789")
542
+ assert list(q.keys()) == ["a", "b"]
543
+ """
524
544
return self ._dict .keys ()
525
545
526
546
def values (self ) -> typing .ValuesView :
547
+ """
548
+ Return all the values in the query params. If a key occurs more than once
549
+ only the first item for that key is returned.
550
+
551
+ Usage:
552
+
553
+ q = httpx.QueryParams("a=123&a=456&b=789")
554
+ assert list(q.values()) == ["123", "789"]
555
+ """
527
556
return {k : v [0 ] for k , v in self ._dict .items ()}.values ()
528
557
529
558
def items (self ) -> typing .ItemsView :
530
559
"""
531
560
Return all items in the query params. If a key occurs more than once
532
561
only the first item for that key is returned.
562
+
563
+ Usage:
564
+
565
+ q = httpx.QueryParams("a=123&a=456&b=789")
566
+ assert list(q.items()) == [("a", "123"), ("b", "789")]
533
567
"""
534
568
return {k : v [0 ] for k , v in self ._dict .items ()}.items ()
535
569
536
570
def multi_items (self ) -> typing .List [typing .Tuple [str , str ]]:
537
571
"""
538
572
Return all items in the query params. Allow duplicate keys to occur.
573
+
574
+ Usage:
575
+
576
+ q = httpx.QueryParams("a=123&a=456&b=789")
577
+ assert list(q.multi_items()) == [("a", "123"), ("a", "456"), ("b", "789")]
539
578
"""
540
579
multi_items : typing .List [typing .Tuple [str , str ]] = []
541
580
for k , v in self ._dict .items ():
@@ -546,31 +585,93 @@ def get(self, key: typing.Any, default: typing.Any = None) -> typing.Any:
546
585
"""
547
586
Get a value from the query param for a given key. If the key occurs
548
587
more than once, then only the first value is returned.
588
+
589
+ Usage:
590
+
591
+ q = httpx.QueryParams("a=123&a=456&b=789")
592
+ assert q.get("a") == "123"
549
593
"""
550
594
if key in self ._dict :
551
- return self ._dict [key ][0 ]
595
+ return self ._dict [str ( key ) ][0 ]
552
596
return default
553
597
554
598
def get_list (self , key : typing .Any ) -> typing .List [str ]:
555
599
"""
556
600
Get all values from the query param for a given key.
601
+
602
+ Usage:
603
+
604
+ q = httpx.QueryParams("a=123&a=456&b=789")
605
+ assert q.get_list("a") == ["123", "456"]
557
606
"""
558
- return list (self ._dict .get (key , []))
607
+ return list (self ._dict .get (str ( key ) , []))
559
608
560
- def update (self , params : QueryParamTypes = None ) -> None :
561
- if not params :
562
- return
609
+ def set (self , key : typing .Any , value : typing .Any = None ) -> "QueryParams" :
610
+ """
611
+ Return a new QueryParams instance, setting the value of a key.
612
+
613
+ Usage:
614
+
615
+ q = httpx.QueryParams("a=123")
616
+ q = q.set("a", "456")
617
+ assert q == httpx.QueryParams("a=456")
618
+ """
619
+ q = QueryParams ()
620
+ q ._dict = dict (self ._dict )
621
+ q ._dict [str (key )] = [primitive_value_to_str (value )]
622
+ return q
623
+
624
+ def add (self , key : typing .Any , value : typing .Any = None ) -> "QueryParams" :
625
+ """
626
+ Return a new QueryParams instance, setting or appending the value of a key.
563
627
564
- params = QueryParams (params )
565
- for k in params .keys ():
566
- self ._dict [k ] = params .get_list (k )
628
+ Usage:
629
+
630
+ q = httpx.QueryParams("a=123")
631
+ q = q.add("a", "456")
632
+ assert q == httpx.QueryParams("a=123&a=456")
633
+ """
634
+ q = QueryParams ()
635
+ q ._dict = dict (self ._dict )
636
+ q ._dict [str (key )] = q .get_list (key ) + [primitive_value_to_str (value )]
637
+ return q
638
+
639
+ def remove (self , key : typing .Any ) -> "QueryParams" :
640
+ """
641
+ Return a new QueryParams instance, removing the value of a key.
642
+
643
+ Usage:
644
+
645
+ q = httpx.QueryParams("a=123")
646
+ q = q.remove("a")
647
+ assert q == httpx.QueryParams("")
648
+ """
649
+ q = QueryParams ()
650
+ q ._dict = dict (self ._dict )
651
+ q ._dict .pop (str (key ), None )
652
+ return q
653
+
654
+ def merge (self , params : QueryParamTypes = None ) -> "QueryParams" :
655
+ """
656
+ Return a new QueryParams instance, updated with.
657
+
658
+ Usage:
659
+
660
+ q = httpx.QueryParams("a=123")
661
+ q = q.merge({"b": "456"})
662
+ assert q == httpx.QueryParams("a=123&b=456")
663
+
664
+ q = httpx.QueryParams("a=123")
665
+ q = q.merge({"a": "456", "b": "789"})
666
+ assert q == httpx.QueryParams("a=456&b=789")
667
+ """
668
+ q = QueryParams (params )
669
+ q ._dict = {** self ._dict , ** q ._dict }
670
+ return q
567
671
568
672
def __getitem__ (self , key : typing .Any ) -> str :
569
673
return self ._dict [key ][0 ]
570
674
571
- def __setitem__ (self , key : str , value : str ) -> None :
572
- self ._dict [key ] = [value ]
573
-
574
675
def __contains__ (self , key : typing .Any ) -> bool :
575
676
return key in self ._dict
576
677
@@ -580,6 +681,9 @@ def __iter__(self) -> typing.Iterator[typing.Any]:
580
681
def __len__ (self ) -> int :
581
682
return len (self ._dict )
582
683
684
+ def __hash__ (self ) -> int :
685
+ return hash (str (self ))
686
+
583
687
def __eq__ (self , other : typing .Any ) -> bool :
584
688
if not isinstance (other , self .__class__ ):
585
689
return False
@@ -593,6 +697,18 @@ def __repr__(self) -> str:
593
697
query_string = str (self )
594
698
return f"{ class_name } ({ query_string !r} )"
595
699
700
+ def update (self , params : QueryParamTypes = None ) -> None :
701
+ raise RuntimeError (
702
+ "QueryParams are immutable since 0.18.0. "
703
+ "Use `q = q.merge(...)` to create an updated copy."
704
+ )
705
+
706
+ def __setitem__ (self , key : str , value : str ) -> None :
707
+ raise RuntimeError (
708
+ "QueryParams are immutable since 0.18.0. "
709
+ "Use `q = q.set(key, value)` to create an updated copy."
710
+ )
711
+
596
712
597
713
class Headers (typing .MutableMapping [str , str ]):
598
714
"""
0 commit comments