Excluding fields on a pydantic model when it is the nested child of another model

huangapple go评论55阅读模式
英文:

Excluding fields on a pydantic model when it is the nested child of another model

问题

I have a pydantic model that I want to dynamically exclude fields on.

I can do this by overriding the dict function on the model so it can take my custom flag, e.g.:

class MyModel(BaseModel):
  field: str

  def dict(self, **kwargs):
    if ('exclude_special_fields' in kwargs):
       super().dict(exclude={"field": True}, **kwargs)

    super().dict(**kwargs)

However, this does not work if my model is a child of another model that has .dict called on it:

class AnotherModel(BaseModel):
  models: List[MyModel]

AnotherModel(models=[...]).dict(exclude_special_fields=True) # does not work

This is because when MyModel.dict() is called, it isn't called with the same arguments as the parent.

I could write a dict override on the parent model too to specify an exclude for any child components (e.g. exclude={"models": {"__all__": {"field": True}}}), but in my real-world example, I have many parent models that use this one sub-model, and I don't want to have to write an override for each one.

Is there any way I can ensure the child model knows when to exclude fields?

Extra context

Extra context not completely important to the question, but the reason I want to do this is to exclude certain fields on a model if it's ever returned from an API call.

英文:

I have a pydantic model that I want to dynamically exclude fields on.

I can do this by overriding the dict function on the model so it can take my custom flag, e.g.:

class MyModel(BaseModel):
  field: str

  def dict(self, **kwargs):
    if ('exclude_special_fields' in kwargs):
       super().dict(exclude={"field": True}, **kwargs)

    super().dict(**kwargs)

However, this does not work if my model is a child of another model that has .dict called on it:

class AnotherModel(BaseModel):
  models: List[MyModel]

AnotherModel(models=[...]).dict(exclude_special_fields=True) # does not work

This is because when MyModel.dict() is called, it isn't called with the same arguments as the parent.

I could write a dict override on the parent model too to specify an exclude for any child components (e.g. exclude={"models": {"__all__": {"field": True}}}), but in my real world example, I have many parent models that use this one sub-model, and I don't want to have to write an override for each one.

Is there anyway I can ensure the child model knows when to exclude fields?

Extra context

Extra context not completely important to the question, but the reason I want to do this is to exclude certain fields on a model if it's ever returned from an API call.

答案1

得分: 3

After looking through the source code, I don't see any way that this would be easily possible with the specialized kwarg supplied. The dict function is recursive and doesn't support arbitrary arguments.

Now, I was able to hack something together but... it is awful. This was performed using pydantic==1.10.7.

I was thinking we could apply a special flag value in the exclude arguments to provide something to trigger the exclusion logic off of. This became trickier than I expected because it requires knowing the full structure of the object and exactly how to index the excluded fields. There is also some odd normalization happening on lists, which is causing the value provided to be mutated as it makes its way down the function call.

This is the best I could get (WARNING not tested thoroughly). We create a dictionary that returns itself on every lookup, that exposed __all__ as an exclude field. This will allow our key to be passed to each and every model and be passed on to child objects to evaluate as well.

EXCLUDED_SPECIAL_FIELDS = "exclude_special_fields"

class _ExclusionDict(dict):
    def __init__(self):
        super().__init__({"__all__": {EXCLUDED_SPECIAL_FIELDS: True}})

    def get(self, key):
        return self


ExcludeSpecial = _ExclusionDict()


class SpecialExclusionBaseModel(BaseModel):
    _special_exclusions: set[str]

    def dict(self, **kwargs):
        exclusions = getattr(self.__class__, "_special_exclusions", None)
        exclude = kwargs.get("exclude")
        if exclusions and exclude and EXCLUDED_SPECIAL_FIELDS in exclude:
            return {
                k: v
                for k, v in super().dict(**kwargs).items()
                if k not in exclusions
            }
        return super().dict(**kwargs)

With this base class, we can provide a class field called _special_exclusions to indicate which fields we want excluded when the ExcludeSpecial instance is provided as the exclude kw argument.

On some initial testing, this seems to work with nested hierarchies, including dicts and lists. There are probably bugs here that need to be worked out, but hopefully, this is a good jumping-off point for others.

class MyModel(SpecialExclusionBaseModel):
    _special_exclusions = {"field"}
    field: str

class AnotherModel(BaseModel):
    models: list[MyModel]

class AnotherAnotherModel(BaseModel):
    models: dict[str, AnotherModel]


model = MyModel(field=1)
another = AnotherAnotherModel(models={"test": AnotherModel(models=[model])})
print(another.dict(exclude=ExcludeSpecial))
{'models': {'test': {'models': [{}]}}}
英文:

After looking through the source code, I don't see any way that this would be easily possible with the specialized kwarg supplied. The dict function is recursive and doesn't support arbitrary arguments.

Now, I was able to hack something together but... it is awful. This was performed using pydantic==1.10.7.

I was thinking we could apply a special flag value in the exclude arguments to provide something to trigger the exclusion logic off of. This became trickier than I expected, because it requires knowing the full structure of the object and exactly how to index the excluded fields. There is also some odd normalization happening on lists, which is causing the value provided to be mutated as it makes its way down the function call.

This is the best I could get (WARNING not tested thoroughly). We create a dictionary that returns itself on every lookup, that exposed __all__ as an exclude field. This will allow our key to be passed to each and every model, and be passed on to child objects to evaluate as well.

EXCLUDED_SPECIAL_FIELDS = "exclude_special_fields"

class _ExclusionDict(dict):
    def __init__(self):
        super().__init__({"__all__": {EXCLUDED_SPECIAL_FIELDS: True}})

    def get(self, key):
        return self


ExcludeSpecial = _ExclusionDict()


class SpecialExclusionBaseModel(BaseModel):
    _special_exclusions: set[str]

    def dict(self, **kwargs):
        exclusions = getattr(self.__class__, "_special_exclusions", None)
        exclude = kwargs.get("exclude")
        if exclusions and exclude and EXCLUDED_SPECIAL_FIELDS in exclude:
            return {
                k: v
                for k, v in super().dict(**kwargs).items()
                if k not in exclusions
            }
        return super().dict(**kwargs)

With this base class, we can provide a class field called _special_exclusions to indicate which fields we want excluded when the ExcludeSpecial instance is provided as the exclude kw argument.

On some initial testing, this seems to work with nested hierarchies including dicts and lists. There are probably bugs here that need to be worked out, but hopefully this is a good jumping off point for others.

class MyModel(SpecialExclusionBaseModel):
    _special_exclusions = {"field"}
    field: str

class AnotherModel(BaseModel):
    models: list[MyModel]

class AnotherAnotherModel(BaseModel):
    models: dict[str, AnotherModel]


model = MyModel(field=1)
another = AnotherAnotherModel(models={"test": AnotherModel(models=[model])})
print(another.dict(exclude=ExcludeSpecial))
{'models': {'test': {'models': [{}]}}}

huangapple
  • 本文由 发表于 2023年4月17日 06:37:11
  • 转载请务必保留本文链接:https://go.coder-hub.com/76030663.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定