Skip to content
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

There should be some way to type a dict as a strict subset of some other TypedDict #7299

Closed
tomoyoirl opened this issue Aug 7, 2019 · 3 comments

Comments

@tomoyoirl
Copy link

tomoyoirl commented Aug 7, 2019

I would like to define a Django application configuration in a type safe way, and provide tools which supply app dependencies to components of the application, like a service locator pattern or something. I am developing code to do make this happen.

Here is the outline of an app config class:

DependencyBundle = TypeVar("DependencyBundle")
class AppConfig(DjangoAppConfig, Generic[DependencyBundle], ABC):
    @classmethod
    def bundle(cls: Type["AppConfig"]) -> DependencyBundle:
       # ... accessor details omitted
    # ... initialization code in `ready()` omitted

Here is an app config instance:

@dataclass
class ContentDependencies:
    content_validator: Validator
    content_loader: Loader
    other_dependency: Any

class ContentConfig(AppConfig[ContentDependencies]):
    name = "organization.content"
    verbose_name = "Content App"
    # ... specifics of bundle initialization omitted

Here is the action base class:

AppConfigType = TypeVar("ConfigType")
ParamType = TypeVar("ParamType")
ReturnType = TypeVar("ReturnType")

class AppAction(Generic[AppConfigType, ParamType, ReturnType], ABC):
    config: AppConfigType

    @classmethod
    def build(cls: Type[ActionType], args: ParamType) -> ActionType:
        return cls(app=cls.config.bundle(), args=args)

    def __init__(self, *, app: AppConfigType, args: ParamType) -> None:
        self.app = app
        self.args = args

    def execute(self) -> ReturnType:
        # ... (skeleton validation code omitted)

Here is an instance of an action. The action class wants to make use of the ContentConfig bundle, but also identify that it depends only on the loader and validator classes, the better to support the Interface Segregation Principle.

class PublishDependencies(Protocol):
    content_loader: Loader
    content_validator: Validator

class PublishArgs(TypedDict):
    url_slug: str
    external_content_id: str

class PublishContent(AppAction[PublishDependencies, PublishArgs, None]):
    app = ContentConfig
    def execute(self) -> None:
        content = self.app.loader(content_id=self.args["content_id"])
        ...

This approach seems to work well so far. However, it is slightly tedious to test this, because we must create an instance of a test-only class that implements the PublishDependencies protocol:

@dataclass
class BundleForTest:
   content_loader: Loader
   content_validator: Validator

class TestApp(TestCase):
   def test_action(self):
      test_action = PublishContent(app=BundleForTest(test_content_loader, test_validator), args={"args": "under test"})

If not for the problem of extra keys being present at runtime, it would be convenient to make both PublishDependencies and ContentDependencies into a TypedDict, instead, which would allow tests to use simple dict objects here instead:

def test_foo(self):
   test_action = PublishContent(app={"content_loader": test_content_loader, "content_validator": test_content_validator}, args={})

I seek some way to identify one typed TypedDict as a strict subset of another, or else a type-safe way to construct a TypedDict from a dict which is a strict superset (without duplicating every key in the child dict).

@ilevkivskyi
Copy link
Member

I seek some way to identify one typed TypedDict as a strict subset of another [...]

I am not sure I completely understand the issue. Could you please give a simplified example of code with typed dicts that currently passes but should fail in your opinion or vice versa (fails but should pass in your opinion).

@tomoyoirl
Copy link
Author

tomoyoirl commented Aug 12, 2019

To highlight the feature without the use case, then, it is well and right that this should fail:

class SmallDict(TypedDict):
  field: str

class BigDict(TypedDict):
  field: str
  other_field: bool

big = BigDict(field="value", other_field=true)
small: SmallDict = big

However, there should be a way to write something similar that succeeds, in the way that this succeeds:

class SmallProto(Protocol):
  field: str

@dataclass
class BigDataclass:
  field: str
  other_field: bool

big = BigDataclass(field="value", other_field=true)
small: SmallProto = big

@ilevkivskyi
Copy link
Member

then, it is well and right that this should fail:

The code you posted actually passes mypy, because typed dicts follow structural subtyping (exactly as protocols do). Maybe you were bitten by some failures with total=False, but then this is a known problem, see #6577

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants