crdoconnor / strictyaml

Type-safe YAML parser and validator.
https://hitchdev.com/strictyaml/
MIT License
1.46k stars 60 forks source link

Support for type hints #90

Open ben-z opened 4 years ago

ben-z commented 4 years ago

Feature request

Does strictyaml work well with type hints? I can't find any documentation for this. I would like to have this functionality so I won't need to define my types twice.

crdoconnor commented 4 years ago

No, not yet, but I'm open to the idea. Can you share some example code that demonstrates the problem?

ben-z commented 4 years ago

My use case is this:

from strictyaml import load as load_yaml, Map

schema = MAP({ ... })

class SomeClass():
  config: schema # I want this part
  def __init__(self, config: schema):
    self.config = config

  @static_method
  def from_yaml(yaml_str: str):
    return SomeClass(load_yaml(yaml_str, schema))

if __name__ == "__main":
  ...
  obj = SomeClass.from_yaml(yaml_str)

Does the above look possible?

opk12 commented 4 years ago

An example the other way around: the program wants a list of TypedDict from a yaml, strictyaml.load(yaml, List[Movie]) would auto-generate a schema.

Based on the PEP 589 (TypeDict) Movies snippet.

from typing import cast, List, Optional, TypedDict

import strictyaml
from strictyaml import Int, Map, Seq, Str

class Location(TypedDict):
    country: str

SCHEMA_LOCATION = Map(
    {
        "country": Str()
    }
)

class Movie(TypedDict):
    name: str
    year: int
    locations: List[Location]
    comments: Optional[List[str]]

SCHEMA_MOVIE = Map(
    {
        "name": Str(),
        "year": Int(),
        "locations": Seq(SCHEMA_LOCATION),
        strictyaml.Optional("comments", default=None): Seq(Str()),
    }
)

def movies(yaml: str) -> List[Movie]:
    # current, working code:
    data = strictyaml.load(yaml, schema=Seq(SCHEMA_MOVIE)).data
    return cast(List[Movie], data)

    # ideally it would be:
    return strictyaml.load(yaml, hint=List[Movie])

These are minor issues, but it would be nice if I could avoid

  1. type information duplication
  2. cast()
  3. looking up the docs for the strictyaml equivalents of typing's Union, etc

strictyaml.Optional is qualified above, for the collision with typing.Optional.

(Edit) For runtime introspection of List and Dict, Python >= 3.8 has typing.get_origin and typing.get_args, which were added in Python 3.8. For Python < 3.8, there are snippets in typing_inspect issue #37.

For runtime introspection of TypedDict, the typing docs says that the type info is in __annotations__ and __total__ (totality is defined in the PEP).

In this example, load() would detect a List [1] of something that has __annotations__ [2].

[1] List.__origin__ == list should work, according to the typing module source code, but maybe a better way is documented somewhere. [2] per PEP 3107 § Accessing Function Annotations

opk12 commented 4 years ago

For completeness and myself: the rationale for the cast above is that .data contains just other dicts, lists and strings/ints/etc, as opposed to custom containers, and TypedDict is essentially a constructor and type hint for a plain dict (PEP: the runtime type of a TypedDict object will always be just dict (it is never a subclass of dict)).

(edit) Strictly speaking, that cast is incorrect with the current strictyaml implementation, that makes List[OrderedDict] (#80). In fact, PEP 484 section "Covariance and contravariance" lets a client of List[Dict] rely on type() being exactly dict, as opposed to a subclass. A workaround might be (#80) to return dict for load(..., List[Dict]) and OrderedDict for load(..., Seq(Map())).

RichardFevrier commented 1 year ago

👀