litestar-org / litestar

Production-ready, Light, Flexible and Extensible ASGI API framework | Effortlessly Build Performant APIs
https://litestar.dev/
MIT License
5.38k stars 364 forks source link

Enhancement: support DTO config inheritance #2760

Open peterschutt opened 9 months ago

peterschutt commented 9 months ago

When inheriting a DTO factory class, there is no way to merge the config of the inherited type with a config defined on the new class.

One idea is to have a callback method on AbstractDTO that handles the case where a 2nd config object is found in the mro of the DTO type.. something like:

class AbstractDTO:
    ...

    @staticmethod
    def merge_configs(old: DTOConfig, new: DTOConfig) -> DTOConfig:
        return new

Default would just return the latest one, which would preserve current overwrite behavior, but then you could do something like:

class MyDTO(PydanticDTO[...]):
    ...

    @staticmethod
    def merge_configs(old: DTOConfig, new: DTOConfig) -> DTOConfig:
        new.rename.update(old.rename)
        return new

[!NOTE]
While we are open for sponsoring on GitHub Sponsors and OpenCollective, we also utilize Polar.sh to engage in pledge-based sponsorship.

Check out all issues funded or available for funding on our Polar.sh Litestar dashboard

  • If you would like to see an issue prioritized, make a pledge towards it!
  • We receive the pledge once the issue is completed & verified
  • This, along with engagement in the community, helps us know which features are a priority to our users.

Fund with Polar

peterschutt commented 9 months ago

Other approaches I can think of:

class MyDTO(PydanticDTO[...]):
    default_configs = [
        DefaultConfig(name="exclude", value={"id"}, strategy="merge"),
    ]

Or, have the config object itself receive a merge callback that is called whenever we detect a config object is about to be overwritten by another config object in the MRO.

def merge_callback(self: DTOConfig, new: DTOConfig) -> DTOConfig:
    return new.replace(exclude=self.exclude | new.exclude)

class BaseDTO(SQLAlchemyDTO[T]):
    config = DTOConfig(exclude={"id"}, merge_callback=merge_callback)
provinzkraut commented 9 months ago

Other approaches I can think of:

class MyDTO(PydanticDTO[...]):
    default_configs = [
        DefaultConfig(name="exclude", value={"id"}, strategy="merge"),
    ]

What other strategies than merge would be useful to have as a default?

Or, have the config object itself receive a merge callback that is called whenever we detect a config object is about to be overwritten by another config object in the MRO.

def merge_callback(self: DTOConfig, new: DTOConfig) -> DTOConfig:
    return new.replace(exclude=self.exclude | new.exclude)

class BaseDTO(SQLAlchemyDTO[T]):
    config = DTOConfig(exclude={"id"}, merge_callback=merge_callback)

I like the idea of being able to supply a callable somehow, but I'm not sure if a method on the DTO or a callback is better :shrug:

The callback would allow a mixed approach though:

DTOConfig(..., merge_configs="merge")
DTOConfig(..., merge_configs="update")

def merge_configs(self: DTOConfig, other: DTOConfig) -> DTOConfig:
  return other.replace(exclude=self.exclude | other.exclude)

DTOConfig(..., merge_configs=merge_configs)