New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support Enum query params #2515
Conversation
@@ -554,7 +554,6 @@ def __init__( | |||
|
|||
value = args[0] if args else kwargs | |||
|
|||
items: typing.Sequence[typing.Tuple[str, PrimitiveData]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was unused.
Should we support auto-casting enums here, or should we raise a type error if you pass anything other than a 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 |
elif isinstance(value, enum.Enum): | ||
return str(value.value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this imply that the PrimitiveData
type ought to change to include enum.Enum
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I miss that.
Do you think that enum.Enum
should not be considered a primitive data, and hence not be in this function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Honest answer?
My preference would be for us to raise nice tight TypeError
cases here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For a similar example...
import json
json.dumps{"value": Color.RED} # TypeError: Object of type Color is not JSON serializable
json.dumps{"value": Color.RED.value} # '{"value": 1}'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The thing is... I think the following should raise an error:
class Color(enum.Enum):
RED = 1
GREEN = 2
BLUE = 3
But not the following:
class Color(int, enum.Enum):
RED = 1
GREEN = 2
BLUE = 3
Because the int
there means that each element is an integer, analogous to other types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hrm. That's a bit awkward...
class Color(str, enum.Enum):
# heeeeyyyyyy I'm just a normal str
RED = "red"
GREEN = "green"
BLUE = "blue"
c = Color.RED
isinstance(c, str) # True
str(c) # 'Color.RED' yeah but you ain't
The upshot is even if you're type-checking the user still needs to correctly pass Color.RED.value
, unless we special-case for enum
.
Maybe the upshot is we should have strict type checking here and that we should also expand the primitive types to include enum.Enum
. (Just because we're friendly like that?)
(Or maybe the upshot is that users should pass the enum value, because we're cold hard calculating types, and we like precision)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The upshot is even if you're type-checking the user still needs to correctly pass Color.RED.value, unless we special-case for enum.
If I understood correctly what you said, this is not the case. Type checkers understand that each member of a (str, Enum) is a str, and they don't raise errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think requests has the same behavior as httpx here, does it? Because the Testclient changed, so I assume requests can do the "right" thing on (str, Enum) - I'm on my phone.
Currently both >>> 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 |
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. 🤔 |
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 It's not obvious to me that we should be special casing 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
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 >>> 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 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 |
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 |
@@ -67,6 +68,8 @@ def primitive_value_to_str(value: "PrimitiveData") -> str: | |||
return "false" | |||
elif value is None: | |||
return "" | |||
elif isinstance(value, enum.Enum): | |||
return str(value.value) | |||
return str(value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'd be happier if HTTPX had an extra branch if isinstance(value, str): return value
here, and then raised an explicit TypeError
for all remaining cases.
For Enum
, we could assume that people want value.value
to be used, but why assume and potentially cause unexpected behavior when we can make users provide exactly what they want with a few keystrokes?
Is my thinking, at least
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The if isinstance(value, str): return value
solves the issue that I want to solve here.
Should I proceed? Aren't we going to have some side effects?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was kind of what happened on #2523, and we had to revert.
Closing as stale |
Raised by mlrun/mlrun#2629.