httpx
httpx copied to clipboard
Support Enum query params
Raised by https://github.com/mlrun/mlrun/pull/2629.
Should we support auto-casting enums here, or should we raise a type error if you pass anything other than a boolean/int/float/None?
Option 1.
Explicit type error
import enum
import httpx
class Color(enum.Enum):
RED = 1
GREEN = 2
BLUE = 3
u = httpx.URL("https://example.com", params={"color": Color.RED.value})
print(u) # https://example.com?color=1
u = httpx.URL("https://example.com", params={"color": Color.RED})
# TypeError: Invalid type for params value. Expected str, bool, int, or float, got <enum 'Color'>: <Color.RED: 1>
Option 2.
Silent type coercion
import enum
import httpx
class Color(enum.Enum):
RED = 1
GREEN = 2
BLUE = 3
u = httpx.URL("https://example.com", params={"color": Color.RED.value})
print(u) # https://example.com?color=1
u = httpx.URL("https://example.com", params={"color": Color.RED})
print(u) # https://example.com?color=1
Currently both requests and httpx will perform silent type coercion to str here, but neither special-cases the enum type, so both give an awkward behaviour...
>>> r = requests.get("https://www.example.com", params={"a": Color.RED})
>>> r.request.url
'https://www.example.com/?a=Color.RED'
Edit: Ah okay, the behaviour there depends on if you're using Color(enum.Enum) or Color(str, enum.Enum).
Should we support auto-casting enums here, or should we raise a type error if you pass anything other than a boolean/int/float/None?
Hmm... Maybe this would be better:
if isinstance(value, enum.Enum):
return primitive_value_to_str(value.value)
Then it would support all those cases. :thinking:
Okay, I've some reluctance on this, which I'm more clear now on how to phrase.
It's about consistency.
class SiteURLs(str, enum.Enum):
staging = "https://localhost"
production = "https://www.example.com"
httpx.URL(SiteURLs.staging)
# Nope, not going to do what you want.
# You meant to pass `SiteURLs.staging.value`.
class ContentType(str, enum.Enum):
json = "application/json"
form = "application/x-www-form-urlencoded"
httpx.Headers({"Content-Type": ContentType.json})
# Nope, not going to do what you want.
# You meant to pass `ContentType.json.value`.
Same with, say json.dumps({...}) and other APIs.
It's not obvious to me that we should be special casing Enum in QueryParams, when the less ambiguous API usage is for the user to pass the .value from enum instances.
On the other hand... I can see that the less verbose style feels nicer. It's just that it's less precise and in other cases you might well end up with the __str__ representation.
Because the Testclient changed
I saw something along those lines mentioned in the referenced ticket... but I wasn't clear what it meant, and I couldn't replicated with requests. For example:
>>> r = requests.get("https://www.example.com", params={"a": Color.RED})
>>> r.request.url
'https://www.example.com/?a=Color.RED' # Ooops you meant `Color.RED.value`. Same as with `httpx`.
@tomchristie did your thoughts regarding this changed or should I close it?
It's really not obvious what behaviour you should expect when using a StrEnum if you don't access .value. For example...
class CustomEnum(str, enum.Enum):
A = "a"
B = "b"
"%s" % CustomEnum.A
'CustomEnum.A'
"".join([CustomEnum.A])
'a'
😬
Anyways we probably just need to suck this weirdness up and deal with it...
def primitive_value_to_str(value: "PrimitiveData") -> str:
"""
Coerce a primitive data type into a string value.
Note that we prefer JSON-style 'true'/'false' for boolean values here.
"""
if value is True:
return "true"
elif value is False:
return "false"
elif value is None:
return ""
elif isinstance(value, (str, int)):
if isinstance(value, enum.Enum):
# Typed enums have some pretty inconsistent
# behaviour around expectations when passed directly.
#
# For example, see...
# https://github.com/encode/httpx/pull/2515#issuecomment-1373705563
#
# We could either raise an exception here and force
# developers to pass `.value` directly, or just coerce it.
# We're being soft on ya here...
return str(value.value)
return str(value)
# Passing other types here isn't type-correct against our API,
# but we do currently automatically coerce them.
#
# There has been some discussion about removing this in favour
# stricter typing. See this reverted pull request for more details...
# https://github.com/encode/httpx/pull/2523
return str(value)
Perhaps this is overkill on the commenting, but at least makes clear where the nasty edges are.
Why only int and str?
note also 3.11 brings backward breaking changes if used with a mixin int or str and the format funciton, might not faciitate the maintenance and deal with all cases
see https://blog.pecar.me/python-enum for a certainly better explanation
Closing as stale