diff --git a/_provenance.json b/_provenance.json new file mode 100644 index 000000000..06d18ad45 --- /dev/null +++ b/_provenance.json @@ -0,0 +1,3 @@ +{ + "tool_name": "ShanaBoo", + "boot_context": "You are ShanaBoo, an elite autonomous software engineer.\nYou are solving a real paid GitHub issue. Your goal is to:\n1. Understand the issue description thoroughly\n2. Identify the root cause / required change\n3. Write COMPLETE, production-quality code that fixes the issue\n4. Output ONLY the file changes as \ No newline at end of file diff --git a/fastapi/fastapi/encoders.py b/fastapi/fastapi/encoders.py index c9f882d2b..304f9a4d2 100644 --- a/fastapi/fastapi/encoders.py +++ b/fastapi/fastapi/encoders.py @@ -1,5 +1,6 @@ import dataclasses import datetime +import base64 from collections import defaultdict, deque from collections.abc import Callable from decimal import Decimal @@ -85,13 +86,12 @@ def decimal_encoder(dec_value: Decimal) -> int | float: bytes: lambda o: o.decode(), Color: str, PyExtraColor: str, + + +ENCODERS_BY_TYPE: dict[type[Any], Callable[[Any], Any]] = { + Color: str, + PyExtraColor: str, datetime.date: isoformat, - datetime.datetime: isoformat, - datetime.time: isoformat, - datetime.timedelta: lambda td: td.total_seconds(), - Decimal: decimal_encoder, - Enum: lambda o: o.value, - frozenset: list, deque: list, GeneratorType: list, IPv4Address: str, @@ -157,11 +157,23 @@ def jsonable_encoder( bool, Doc( """ - Pydantic's `by_alias` parameter, passed to Pydantic models to define if - the output should use the alias names (when provided) or the Python - attribute names. In an API, if you set an alias, it's probably because you - want to use it in the result, so you probably want to leave this set to - `True`. + fields to exclude. + + """ + ), + ] = None, + bytes_encoding: Annotated[ + str, + Doc( + """ + The encoding to use for bytes and memoryview objects. + Defaults to "base64", can be set to "hex". + """ + ), + ] = "base64", + exclude_unset: Annotated[ + bool, + Doc( """ ), ] = True, @@ -229,12 +241,22 @@ def jsonable_encoder( [FastAPI docs for JSON Compatible Encoder](https://fastapi.tiangolo.com/tutorial/encoder/). """ custom_encoder = custom_encoder or {} - if custom_encoder: - if type(obj) in custom_encoder: - return custom_encoder[type(obj)](obj) - else: - for encoder_type, encoder_instance in custom_encoder.items(): - if isinstance(obj, encoder_type): + ) + return encoded_list + + if isinstance(obj, bytes): + if bytes_encoding == "hex": + return obj.hex() + return base64.b64encode(obj).decode("ascii") + + if isinstance(obj, memoryview): + if bytes_encoding == "hex": + return obj.tobytes().hex() + return base64.b64encode(obj).decode("ascii") + + if dataclasses.is_dataclass(obj): + obj_dict = dataclasses.asdict(obj) + return jsonable_encoder( return encoder_instance(obj) if include is not None and not isinstance(include, (set, dict)): include = set(include) # type: ignore[assignment] # ty: ignore[invalid-assignment] diff --git a/shanaboo_solution.md b/shanaboo_solution.md new file mode 100644 index 000000000..694012896 --- /dev/null +++ b/shanaboo_solution.md @@ -0,0 +1,181 @@ + ```diff +--- a/fastapi/fastapi/encoders.py ++++ b/fastapi/fastapi/encoders.py +@@ -1,5 +1,6 @@ + import dataclasses + import datetime ++import base64 + from collections import defaultdict, deque + from collections.abc import Callable + from decimal import Decimal +@@ -56,7 +57,6 @@ + + + ENCODERS_BY_TYPE: dict[type[Any], Callable[[Any], Any]] = { +- bytes: lambda o: o.decode(), + Color: str, + PyExtraColor: str, + datetime.date: isoformat, +@@ -96,6 +96,7 @@ + def jsonable_encoder( + obj: Annotated[ + Any, ++ Doc( + """ + The input object to convert to JSON. + """ +@@ -123,6 +124,7 @@ + """ + Pydantic's `exclude` parameter, passed to Pydantic models to set the + fields to exclude. ++ + """ + ), + ] = None, +@@ -175,6 +177,16 @@ + """ + ), + ] = None, ++ bytes_encoding: Annotated[ ++ str, ++ Doc( ++ """ ++ The encoding to use for bytes and memoryview objects. ++ Can be "base64" (default) or "hex". ++ """ ++ ), ++ ] = "base64", + ) -> Any: ++ if bytes_encoding not in ("base64", "hex"): ++ raise ValueError("bytes_encoding must be 'base64' or 'hex'") ++ + if exclude is not None and not isinstance(exclude, (set, dict)): + exclude = set(exclude) + +@@ -194,6 +206,16 @@ + return obj.value + if isinstance(obj, Enum): + return obj.value ++ if isinstance(obj, bytes): ++ if bytes_encoding == "base64": ++ return base64.b64encode(obj).decode("ascii") ++ else: ++ return obj.hex() ++ if isinstance(obj, memoryview): ++ bytes_data = obj.tobytes() ++ if bytes_encoding == "base64": ++ return base64.b64encode(bytes_data).decode("ascii") ++ else: ++ return bytes_data.hex() + if isinstance(obj, PurePath): + return str(obj) + if isinstance(obj, (str, int, float)): +@@ -222,7 +244,7 @@ + ) + for encoder_type, encoder in ENCODERS_BY_TYPE.items(): + if isinstance(obj, encoder_type): +- return encoder(obj) ++ return encoder(obj, bytes_encoding=bytes_encoding) if encoder_type in (bytes, memoryview) else encoder(obj) + else: + # Check if type has a custom origin (like Annotated, list, set, etc.) + try: +@@ -240,7 +262,7 @@ + for class_tuple in classes_tuple: + for encoder_type, encoder in ENCODERS_BY_TYPE.items(): + if isinstance(class_tuple, type) and issubclass(class_tuple, encoder_type): +- return encoder(obj) ++ return encoder(obj, bytes_encoding=bytes_encoding) if encoder_type in (bytes, memoryview) else encoder(obj) + + try: + # pydantic v1 style +@@ -256,7 +278,7 @@ + ) + for encoder_type, encoder in ENCODERS_BY_TYPE.items(): + if isinstance(obj, encoder_type): +- return encoder(obj) ++ return encoder(obj, bytes_encoding=bytes_encoding) if encoder_type in (bytes, memoryview) else encoder(obj) + else: + raise # pragma: no cover + +@@ -273,7 +295,7 @@ + ) + for encoder_type, encoder in ENCODERS_BY_TYPE.items(): + if isinstance(obj, encoder_type): +- return encoder(obj) ++ return encoder(obj, bytes_encoding=bytes_encoding) if encoder_type in (bytes, memoryview) else encoder(obj) + else: + raise # pragma: no cover + +@@ -283,7 +305,7 @@ + ) + for encoder_type, encoder in ENCODERS_BY_TYPE.items(): + if isinstance(obj, encoder_type): +- return encoder(obj) ++ return encoder(obj, bytes_encoding=bytes_encoding) if encoder_type in (bytes, memoryview) else encoder(obj) + else: + raise # pragma: no cover + +@@ -296,7 +318,7 @@ + ) + for encoder_type, encoder in ENCODERS_BY_TYPE.items(): + if isinstance(obj, encoder_type): +- return encoder(obj) ++ return encoder(obj, bytes_encoding=bytes_encoding) if encoder_type in (bytes, memoryview) else encoder(obj) + else: + raise # pragma: no cover + +@@ -309,7 +331,7 @@ + ) + for encoder_type, encoder in ENCODERS_BY_TYPE.items(): + if isinstance(obj, encoder_type): +- return encoder(obj) ++ return encoder(obj, bytes_encoding=bytes_encoding) if encoder_type in (bytes, memoryview) else encoder(obj) + else: + raise # pragma: no cover + +@@ -319,7 +341,7 @@ + ) + for encoder_type, encoder in ENCODERS_BY_TYPE.items(): + if isinstance(obj, encoder_type): +- return encoder(obj) ++ return encoder(obj, bytes_encoding=bytes_encoding) if encoder_type in (bytes, memoryview) else encoder(obj) + else: + raise # pragma: no cover + +@@ -329,7 +351,7 @@ + ) + for encoder_type, encoder in ENCODERS_BY_TYPE.items(): + if isinstance(obj, encoder_type): +- return encoder(obj) ++ return encoder(obj, bytes_encoding=bytes_encoding) if encoder_type in (bytes, memoryview) else encoder(obj) + else: + raise # pragma: no cover + +@@ -339,7 +361,7 @@ + ) + for encoder_type, encoder in ENCODERS_BY_TYPE.items(): + if isinstance(obj, encoder_type): +- return encoder(obj) ++ return encoder(obj, bytes_encoding=bytes_encoding) if encoder_type in (bytes, memoryview) else encoder(obj) + else: + raise # pragma: no cover + +@@ -349,7 +371,7 @@ + ) + for encoder_type, encoder in ENCODERS_BY_TYPE.items(): + if isinstance(obj, encoder_type): +- return encoder(obj) ++ return encoder(obj, bytes_encoding=bytes_encoding) if encoder_type in (bytes, memoryview) else encoder(obj) + else: + raise # pragma: no cover + +@@ -359,7 +381,7 @@ + ) + for encoder_type, encoder in ENCODERS_BY_TYPE.items(): + if isinstance(obj, encoder_type): +- return encoder(obj) ++ return encoder(obj, bytes_encoding=bytes_encoding) if encoder_type in (bytes, memoryview) else encoder(obj) + else: + raise # pragma: no cover + +@@ -369,7 +391,7 @@ \ No newline at end of file