-
Notifications
You must be signed in to change notification settings - Fork 16
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
add docs for GroupedMetadata and BaseMetadata #15
Conversation
OK, here's my current proposal:
I guess if |
Interesting, I was not aware of
I would like @samuelcolvin to chime in, but my thought was that in Pydantic v2
I think both are valid
Agreed we should document this, if nothing else because we already use All in all, here was my idea of how a consumer would generally use the inheritance we provide: from dataclasses import dataclass
from typing import Annotated, Any, Iterable, Iterator, get_args, get_origin, Unpack
from annotated_types import BaseMetadata, GroupedMetadata, Len
# a generic flattener, maybe we provide this in the future?
def flatten_metadata(args: Iterable[Any]) -> Iterable[BaseMetadata]:
for arg in args:
if isinstance(arg, BaseMetadata):
yield arg
elif isinstance(arg, GroupedMetadata):
yield from arg
elif get_origin(arg) is Unpack:
packed_args = iter(get_args(arg))
next(packed_args)
yield from packed_args
# Pydantic classes
@dataclass
class Regex(BaseMetadata):
pattern: str
@dataclass
class Alias(BaseMetadata):
alias: str
@dataclass
class FieldInfo(GroupedMetadata):
min_length: int | None = None
regex: str | None = None
alias: str | None = None
def __iter__(self) -> Iterator[BaseMetadata]:
if self.min_length is not None:
yield Len(self.min_length)
if self.regex is not None:
yield Regex(self.regex)
if self.alias is not None:
yield Alias(self.alias)
def Field(
min_length: int | None = None,
regex: str | None = None,
alias: str | None = None,
) -> Any:
return FieldInfo(
min_length=min_length,
regex=regex,
alias=alias,
)
# user's code
# notice this is completely backwards compatible with Pydantic v1
PhoneNumber = Annotated[str, Field(min_length=10, alias="phoneNumber")]
# Pydantic's consumer
known_metadata = {
Len,
Regex,
Alias,
}
def build_pydantic_core_schema_from_pep593_metadata(
tp: type, allow_unknown: bool,
) -> Any:
args = get_args(tp)
assert args and args[0] is Annotated
for metadata in flatten_metadata(args[1:]):
if metadata in known_metadata:
# build pydantic-core schema
pass
elif not allow_unknown:
raise TypeError("unknown annotated-types metadata")
else:
pass
# Hypothesis' consumer
known_metadata = {
Len,
Regex,
Alias,
}
supported_packages = {
"annotated_types",
"pydantic",
}
def _(
tp: type
) -> Any:
args = get_args(tp)
assert len(args) > 2 and args[0] is Annotated
for metadata in flatten_metadata(args[2:]):
if metadata in known_metadata:
# do thing
continue
if next(iter(metadata.__module__.split("."))) in supported_packages:
raise RuntimeError(
"Please update hypothesis"
) So While |
Ignore that, I've just seen your question. |
I'm assuming you didn't read my wall of text in the 15 seconds elapsed between my comment and yours 😉 |
Okay, I've now read your comment. I'm happy for pydantic V2 The only edge case I can think of where this might not be desired is when users want to get the |
Remember that we also specify interpretations for I'm kinda torn on the flattener; on one hand it's complicated enough that a flatten-and-canonicalize function would be pretty useful, on the other I want to keep the library as small as possible. I think the argument that it's complicated wins here and we should ship it. Note that this should actually return
My argument is that you always need a mechanism to do the exhaustive check anyway, and should avoid having additional ways to check metadata (which could then get out of sync, etc). Caching is a sufficient answer to perf concerns here IMO.
If we specify that:
then it will "just work" without requiring consumers to have any knowledge whatsoever of Pydantic. However, the The best alternative is probably "just have and tell people to inherit from a
I don't really care about errors from such consumers, they're already living in a world where anyone can put anything in an
You could have I was imagining that the unpacked constraints would fully describe the Field though, including if necessary a "FieldResidue" object to hold all the other attributes. |
Those are always going to be special cased, I was omitting for brevity.
I'm not arguing that we should ship it or not, I'm just sketching how it might look if we ship it or if Pydantic/Hypothesis implement it.
My concern isn't about performance but rather about usability. Yes, you always have to do some sort of exhaustive check at the end, but it's much easier to handle filtering, raising errors for unknown metadata, etc. when you can layer these things (e.g. filter out all metadata that is unrelated to
@samuelcolvin can comment in more detail but this would be breaking change for Pydantic
I'm lost here, sorry |
OK, yeah, never mind my speculative stuff, |
I did the following test: from typing import Annotated, Any, get_args, get_type_hints
import typing
class ArbitraryIterable:
def __iter__(self):
return iter(["foo"])
def foo(
unpack: Annotated[Any, typing.Unpack[ArbitraryIterable()]],
star: Annotated[Any, *ArbitraryIterable()]
) -> Any:
...
print(get_args(get_type_hints(foo, include_extras=True)["unpack"])[1]) # *<__main__.ArbitraryIterable object at 0x102a94ad0>
print(get_args(get_type_hints(foo, include_extras=True)["star"])[1]) # foo Maybe In any case, I think it's a lot easier for us to say "consumers should iterate over any subclass of |
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 I'll personally check for BaseMetadata
, but I've been convinced that this design makes sense. Let's ship it!
And 💖 thanks again Adrian (and Samuel!) for the conversations, they can be wearing at times but I do think we get better designs out the other end. Or in some cases keep better designs 😅
Discussions are good @Zac-HD, thank you for the pushback! @samuelcolvin I'll let you review this before we merge since this will be the last PR before shipping this (after which it would be harder to turn back!) |
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.
Okay, I've slept and consumed coffee and actually reviewed this. Sorry for hurried answer yesterday 🙈.
Over all I'm in favour of this except I don't think we should encourage libraries to inherit from BaseMetadata
as argued by @Zac-HD. It's worth noting that we could add that encouragement in a future release, but removing it/moving to discouraging inheritance from BaseMetadata
is harder.
Still, @adriangb if you're convinced about this, I'm easy either way.
Encouraging inheritance from GroupedMetadata
makes lots of sense - the other route would be to have some dunder attribute which identified a metadata group which wouldn't require annotated-types
to be installed and imported in order to mark a metadata group. But since pydantic will have annotated-types
as a required dependency, I'm happy with an abc as marker, it's definitely more canonical than a marker attribute.
Co-authored-by: Samuel Colvin <samcolvin@gmail.com>
Co-authored-by: Samuel Colvin <samcolvin@gmail.com>
Codecov Report
@@ Coverage Diff @@
## main #15 +/- ##
==========================================
+ Coverage 95.89% 96.07% +0.18%
==========================================
Files 2 2
Lines 146 153 +7
Branches 36 39 +3
==========================================
+ Hits 140 147 +7
Misses 6 6
📣 Codecov can now indicate which changes are the most critical in Pull Requests. Learn more |
My opinion is that yes, we should encourage it, but since I'm in the minority here I'm happy to punt this down the road. My next question then is: should we explicitly tell others to not inherit from the class and say that it should only be used to recognize metadata coming from |
Let's punt on it, we can always work out what we're doing later 😆 |
Ok let's just remove that section. I'm taking a flight / vacation so not sure how soon I'll get around to it, if either of you want to push the changes please feel free! |
We're just punting on this, and might revisit later - but for now making no API promises is the most flexible way to avoid compat issues in future.
Co-authored-by: Samuel Colvin <samcolvin@gmail.com> Co-authored-by: Zac Hatfield-Dodds <zac.hatfield.dodds@gmail.com>
No description provided.