Giter Club home page Giter Club logo

trycast's Introduction

trycast

Trycast helps parses JSON-like values whose shape is defined by typed dictionaries (TypedDicts) and other standard Python type hints.

You can use the trycast(), checkcast(), or isassignable() functions below for parsing:

trycast()

Here is an example of parsing a Point2D object defined as a TypedDict using trycast():

from bottle import HTTPResponse, request, route  # Bottle is a web framework
from trycast import trycast
from typing import TypedDict

class Point2D(TypedDict):
    x: float
    y: float
    name: str

@route('/draw_point')
def draw_point_endpoint() -> HTTPResponse:
    request_json = request.json  # type: object
    if (point := trycast(Point2D, request_json)) is None:
        return HTTPResponse(status=400)  # Bad Request
    draw_point(point)  # type is narrowed to Point2D
    return HTTPResponse(status=200)

def draw_point(point: Point2D) -> None:
    ...

In this example the trycast function is asked to parse a request_json into a Point2D object, returning the original object (with its type narrowed appropriately) if parsing was successful.

More complex types can be parsed as well, such as the Shape in the following example, which is a tagged union that can be either a Circle or Rect value:

from bottle import HTTPResponse, request, route
from trycast import trycast
from typing import Literal, TypedDict

class Point2D(TypedDict):
    x: float
    y: float

class Circle(TypedDict):
    type: Literal['circle']
    center: Point2D  # a nested TypedDict!
    radius: float

class Rect(TypedDict):
    type: Literal['rect']
    x: float
    y: float
    width: float
    height: float

Shape = Circle | Rect  # a Tagged Union!

@route('/draw_shape')
def draw_shape_endpoint() -> HTTPResponse:
    request_json = request.json  # type: object
    if (shape := trycast(Shape, request_json)) is None:
        return HTTPResponse(status=400)  # Bad Request
    draw_shape(shape)  # type is narrowed to Shape
    return HTTPResponse(status=200)  # OK

Important: Current limitations in the mypy typechecker require that you add an extra cast(Optional[Shape], ...) around the call to trycast in the example so that it is accepted by the typechecker without complaining:

shape = cast(Optional[Shape], trycast(Shape, request_json))
if shape is None:
    ...

These limitations are in the process of being resolved by introducing TypeForm support to mypy.

checkcast()

checkcast() is similar to trycast() but instead of returning None when parsing fails it raises an exception explaining why and where the parsing failed.

Here is an example of parsing a Circle object using checkcast():

>>> from typing import Literal, TypedDict
>>> from trycast import checkcast
>>> 
>>> class Point2D(TypedDict):
...     x: float
...     y: float
... 
>>> class Circle(TypedDict):
...     type: Literal['circle']
...     center: Point2D  # a nested TypedDict!
...     radius: float
... 
>>> checkcast(Circle, {"type": "circle", "center": {"x": 1}, "radius": 10})
Traceback (most recent call last):
  ...
trycast.ValidationError: Expected Circle but found {'type': 'circle', 'center': {'x': 1}, 'radius': 10}
  At key 'center': Expected Point2D but found {'x': 1}
    Required key 'y' is missing
>>> 

ValidationError only spends time generating a message if you try to print it or stringify it, so can be cheaply caught if you only want to use it for control flow purposes.

isassignable()

Here is an example of parsing a Shape object defined as a union of TypedDicts using isassignable():

class Circle(TypedDict):
    type: Literal['circle']
    ...

class Rect(TypedDict):
    type: Literal['rect']
    ...

Shape = Circle | Rect  # a Tagged Union!

@route('/draw_shape')
def draw_shape_endpoint() -> HTTPResponse:
    request_json = request.json  # type: object
    if not isassignable(request_json, Shape):
        return HTTPResponse(status=400)  # Bad Request
    draw_shape(request_json)  # type is narrowed to Shape
    return HTTPResponse(status=200)  # OK

Important: Current limitations in the mypy typechecker prevent the automatic narrowing of the type of request_json in the above example to Shape, so you must add an additional cast() to narrow the type manually:

if not isassignable(request_json, Shape):
    ...
shape = cast(Shape, request_json)  # type is manually narrowed to Shape
draw_shape(shape)

These limitations are in the process of being resolved by introducing TypeForm support to mypy.

A better isinstance()

isassignable(value, T) is similar to Python's builtin isinstance() but additionally supports checking against arbitrary type annotation objects including TypedDicts, Unions, Literals, and many others.

Formally, isassignable(value, T) checks whether value is consistent with a variable of type T (using PEP 484 static typechecking rules), but at runtime.

Motivation & Alternatives

Why use trycast?

The trycast module is primarily designed for recognizing JSON-like structures that can be described by Python's typing system. Secondarily, it can be used for recognizing arbitrary structures that can be described by Python's typing system.

Please see Philosophy for more information about how trycast differs from similar libraries like pydantic.

Why use TypedDict?

Typed dictionaries are the natural form that JSON data comes in over the wire. They can be trivially serialized and deserialized without any additional logic. For applications that use a lot of JSON data - such as web applications - using typed dictionaries is very convenient for representing data structures.

If you just need a lightweight class structure that doesn't need excellent support for JSON-serialization you might consider other alternatives for representing data structures in Python such as dataclasses (recommended), named tuples, attrs, or plain classes.

Installation

python -m pip install trycast

Recommendations while using trycast

  • So that trycast() can recognize TypedDicts with mixed required and not-required keys correctly:
    • Use Python 3.9+ if possible.
    • Prefer using typing.TypedDict, unless you must use Python 3.8. In Python 3.8 prefer typing_extensions.TypedDict instead.
    • Avoid using mypy_extensions.TypedDict in general.

Presentations & Videos

A presentation about using trycast to parse JSON was given at the 2021 PyCon US Typing Summit:

2021 PyCon US Typing Summit Presentation

A presentation describing tools that use Python type annotations at runtime, including trycast, was given at the 2022 PyCon US Typing Summit:

2022 PyCon US Typing Summit Presentation

Contributing

Pull requests are welcome! The Python Community Code of Conduct does apply.

You can checkout the code locally using:

git clone [email protected]:davidfstr/trycast.git
cd trycast

Create your local virtual environment to develop in using Poetry:

poetry shell
poetry install

You can run the existing automated tests in the current version of Python with:

make test

You can also run the tests against all supported Python versions with:

make testall

See additional development commands by running:

make help

License

MIT

Feature Reference

Typing Features Supported

  • Scalars
    • bool
    • int
    • float
    • None, type(None)
  • Strings
    • str
  • Raw Collections
    • list, List
    • tuple, Tuple
    • Sequence, MutableSequence
    • dict, Dict
    • Mapping, MutableMapping
  • Generic Collections (including PEP 585)
    • list[T], List[T]
    • tuple[T, ...], Tuple[T, ...]
    • Sequence[T], MutableSequence[T]
    • dict[K, V], Dict[K, V]
    • Mapping[K, V], MutableMapping[K, V]
  • TypedDict
    • typing.TypedDict, typing_extensions.TypedDict (PEP 589)
    • mypy_extensions.TypedDict (when strict=False)
    • –––
    • Required, NotRequired (PEP 655)
    • ReadOnly (PEP 705)
  • Tuples (Heterogeneous)
    • tuple[T1], tuple[T1, T2], tuple[T1, T2, T3], etc
    • Tuple[T1], Tuple[T1, T2], Tuple[T1, T2, T3], etc
  • Unions
    • Union[X, Y]
    • Optional[T]
    • X | Y (PEP 604)
  • Literals
  • Callables
    • Callable
    • Callable[P, R] (where P=[Any]*N and R=Any)
  • NewTypes (when strict=False)
  • Special Types
    • Any
    • Never
    • NoReturn

Type Checkers Supported

Trycast does type check successfully with the following type checkers:

API Reference

trycast API

def trycast(
    tp: TypeForm[T]† | TypeFormString[T]‡,
    value: object,
    /, failure: F = None,
    *, strict: bool = True,
    eval: bool = True
) -> T | F: ...

If value is in the shape of tp (as accepted by a Python typechecker conforming to PEP 484 "Type Hints") then returns it, otherwise returns failure (which is None by default).

This method logically performs an operation similar to:

return value if isinstance(tp, value) else failure

except that it supports many more types than isinstance, including:

  • List[T]
  • Dict[K, V]
  • Optional[T]
  • Union[T1, T2, ...]
  • Literal[...]
  • T extends TypedDict

Similar to isinstance(), this method considers every bool value to also be a valid int value, as consistent with Python typecheckers:

trycast(int, True) -> True
isinstance(True, int) -> True

Note that unlike isinstance(), this method considers every int value to also be a valid float or complex value, as consistent with Python typecheckers:

trycast(float, 1) -> 1
trycast(complex, 1) -> 1
isinstance(1, float) -> False
isinstance(1, complex) -> False

Note that unlike isinstance(), this method considers every float value to also be a valid complex value, as consistent with Python typecheckers:

trycast(complex, 1.0) -> 1
isinstance(1.0, complex) -> False

Parameters:

  • strict --
    • If strict=False then this function will additionally accept mypy_extensions.TypedDict instances and Python 3.8 typing.TypedDict instances for the tp parameter. Normally these kinds of types are rejected with a TypeNotSupportedError because these types do not preserve enough information at runtime to reliably determine which keys are required and which are potentially-missing.
    • If strict=False then NewType("Foo", T) will be treated the same as T. Normally NewTypes are rejected with a TypeNotSupportedError because values of NewTypes at runtime are indistinguishable from their wrapped supertype.
  • eval -- If eval=False then this function will not attempt to resolve string type references, which requires the use of the eval() function. Otherwise string type references will be accepted.

Raises:

  • TypeNotSupportedError --
    • If strict=True and either mypy_extensions.TypedDict or a Python 3.8 typing.TypedDict is found within the tp argument.
    • If strict=True and a NewType is found within the tp argument.
    • If a TypeVar is found within the tp argument.
    • If an unrecognized Generic type is found within the tp argument.
  • UnresolvedForwardRefError -- If tp is a type form which contains a ForwardRef.
  • UnresolvableTypeError -- If tp is a string that could not be resolved to a type.

Footnotes:

checkcast API

def checkcast(
    tp: TypeForm[T]† | TypeFormString[T]‡,
    value: object,
    /, *, strict: bool = True,
    eval: bool = True
) -> T: ...

If value is in the shape of tp (as accepted by a Python typechecker conforming to PEP 484 "Type Hints") then returns it, otherwise raises ValidationError.

This method logically performs an operation similar to:

if isinstance(tp, value):
    return value
else:
    raise ValidationError(tp, value)

except that it supports many more types than isinstance, including:

  • List[T]
  • Dict[K, V]
  • Optional[T]
  • Union[T1, T2, ...]
  • Literal[...]
  • T extends TypedDict

See trycast.trycast() for information about parameters, raised exceptions, and other details.

Raises:

  • ValidationError -- If value is not in the shape of tp.
  • TypeNotSupportedError
  • UnresolvedForwardRefError
  • UnresolvableTypeError

isassignable API

def isassignable(
    value: object,
    tp: TypeForm[T]† | TypeFormString[T]‡,
    /, *, eval: bool = True
) -> TypeGuard[T]: ...

Returns whether value is in the shape of tp (as accepted by a Python typechecker conforming to PEP 484 "Type Hints").

This method logically performs an operation similar to:

return isinstance(value, tp)

except that it supports many more types than isinstance, including:

  • List[T]
  • Dict[K, V]
  • Optional[T]
  • Union[T1, T2, ...]
  • Literal[...]
  • T extends TypedDict

See trycast.trycast(..., strict=True) for information about parameters, raised exceptions, and other details.

Changelog

Future

v1.2.0

  • Add checkcast(), an alternative to trycast() which raises a ValidationError upon failure instead of returning None. (#16)
  • Add support for Python 3.13.
    • Recognize ReadOnly[] from PEP 705. (#25)
  • Add support for Python 3.12.
    • Recognize type statements from PEP 695. (#29)
  • Enhance support for Python 3.11:
    • Recognize special Never values. (#26)
  • Drop support for Python 3.7. (#21)
  • Enforce that calls to trycast() and isassignable() pass the first 2 arguments in positional fashion and not in a named fashion: (#18) (Breaking change)
    • Yes: trycast(T, value), isassignable(value, T)
    • No: trycast(tp=T, value=value), isassignable(value=value, tp=T)

v1.1.0

  • Fix trycast() to recognize TypedDicts with extra keys. (#19)
    • This new behavior helps recognize JSON structures with arbitrary additional keys and is consistent with how static typecheckers treat additional keys.
  • Fix magic wand in logo to look more like a magic wand. (#20)

v1.0.0

  • Extend trycast() to recognize more kinds of types:
    • Extend trycast() to recognize set[T] and Set[T] values.
    • Extend trycast() to recognize frozenset[T] and FrozenSet[T] values.
    • Extend trycast() to recognize Callable and Callable[P, R] types when P and R only contain Any.
    • Extend trycast() to recognize NewType types when strict=False.
    • Extend trycast() to explicitly disallow TypeVar types.
    • Extend trycast() to explicitly disallow unrecognized Generic types.
  • Fix issues with PEP 484 conformance: (Breaking change)
    • bool values are now correctly treated as assignable to int.
    • bool, int, and float values are now correctly treated as assignable to complex.
  • Add support for Python 3.11.
  • Documentation improvements:
    • Add installation instructions.
    • Improve differentiation from similar libraries.
    • Document supported typing features & type checkers.
    • Mention that trycast() and isassignable() accept TypeFormString[T] in addition to TypeForm[T].
    • Add developer documentation.

v0.7.3

  • Support X|Y syntax for Union types from PEP 604.
  • Documentation improvements:
    • Improve introduction.
    • Add API reference.

v0.7.2

  • Add logo.

v0.7.1

  • Upgrade development status from Beta to Production/Stable: 🎉
    • trycast is thoroughly tested.
    • trycast has high code coverage (98%, across Python 3.7-3.10).
    • trycast has been in production use for over a year at at least one company without issues.
    • trycast supports all major Python type checkers (Mypy, Pyright/Pylance, Pyre, Pytype).
    • trycast's initial API is finalized.
  • Fix coverage to be a dev-dependency rather than a regular dependency.

v0.7.0

  • Finalize the initial API:
    • Alter trycast() to use strict=True by default rather than strict=False. (Breaking change)
    • Define trycast's __all__ to export only the trycast and isassignable functions.
  • Add support for additional type checkers, in addition to Mypy:
    • Add support for the Pyright type checker and Pylance language server extension (for Visual Studio Code).
    • Add support for the Pyre type checker.
    • Add support for the Pytype type checker.
  • Extend trycast() to recognize special Any and NoReturn values.
  • Fix trycast() to provide better diagnostic error when given a tuple of types as its tp argument. Was broken in v0.6.0.

v0.6.1

  • Fix trycast(..., eval=False) to not use typing.get_type_hints(), which internally calls eval().
  • Fix trycast() and isassignable() to avoid swallowing KeyboardInterrupt and other non-Exception BaseExceptions.

v0.6.0

  • Extend trycast() to recognize a stringified type argument.
  • Extend trycast() to report a better error message when given a type argument with an unresolved forward reference (ForwardRef).
  • Fix strict argument to trycast to be passed to inner calls of trycast correctly.
    • This also fixes isassignable()'s use of strict matching to be correct.
  • Alter trycast() to interpret a type argument of None or "None" as an alias for type(None), as consistent with PEP 484.
  • Alter TypeNotSupportedError to extend TypeError rather than ValueError. (Breaking change)
    • This is consistent with trycast's and isinstance's behavior of using a TypeError rather than a ValueError when there is a problem with its tp argument.
  • Drop support for Python 3.6. (Breaking change)
    • Python 3.6 is end-of-life.

v0.5.0

  • isassignable() is introduced to the API:
    • isassignable() leverages trycast() to enable type-checking of values against type objects (i.e. type forms) provided at runtime, using the same PEP 484 typechecking rules used by typecheckers such as mypy.
  • Extend trycast() to recognize Required[] and NotRequired[] from PEP 655, as imported from typing_extensions.
  • Extend trycast() to support a strict parameter that controls whether it accepts mypy_extensions.TypedDict or Python 3.8 typing.TypedDict instances (which lack certain runtime type information necessary for accurate runtime typechecking).
    • For now strict=False by default for backward compatibility with earlier versions of trycast(), but this default is expected to be altered to strict=True when/before trycast v1.0.0 is released.
  • Rename primary development branch from master to main.

v0.4.0

  • Upgrade development status from Alpha to Beta:
    • trycast is thoroughly tested.
    • trycast has high code coverage (92% on Python 3.9).
    • trycast has been in production use for over a year at at least one company without issues.
  • Add support for Python 3.10.
  • Setup continuous integration with GitHub Actions, against Python 3.6 - 3.10.
  • Migrate to the Black code style.
  • Introduce Black and isort code formatters.
  • Introduce flake8 linter.
  • Introduce coverage.py code coverage reports.

v0.3.0

  • TypedDict improvements & fixes:
    • Fix trycast() to recognize custom Mapping subclasses as TypedDicts.
  • Extend trycast() to recognize more JSON-like values:
    • Extend trycast() to recognize Mapping and MutableMapping values.
    • Extend trycast() to recognize tuple[T, ...] and Tuple[T, ...] values.
    • Extend trycast() to recognize Sequence and MutableSequence values.
  • Extend trycast() to recognize tuple[T1, T2, etc] and Tuple[T1, T2, etc] values.
  • Documentation improvements:
    • Improve introduction.
    • Outline motivation to use trycast and note alternatives.

v0.2.0

  • TypedDict improvements & fixes:
    • Fix trycast() to recognize TypedDicts from mypy_extensions.
    • Extend trycast() to recognize TypedDicts that contain forward-references to other types.
      • Unfortunately there appears to be no easy way to support arbitrary kinds of types that contain forward-references.
      • In particular {Union, Optional} types and collection types (List, Dict) with forward-references remain unsupported by trycast().
    • Recognize TypedDicts that have mixed required and not-required keys correctly.
      • Exception: Does not work for mypy_extensions.TypedDict or Python 3.8's typing.TypedDict due to insufficient runtime type annotation information.
    • Fix recognition of a total=False TypedDict so that extra keys are disallowed.
  • Alter typing_extensions to be an optional dependency of trycast.

v0.1.0

  • Add support for Python 3.6, 3.7, and 3.9, in addition to 3.8.

v0.0.2

  • Fix README to appear on PyPI.
  • Add other package metadata, such as the supported Python versions.

v0.0.1a

  • Initial release.
  • Supports typechecking all types found in JSON.

trycast's People

Contributors

adamchainz avatar d-k-bo avatar davidfstr avatar rednafi avatar sbdchd avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

trycast's Issues

Recognize generic TypedDicts, like Point[int]

Python 3.11 added support for generic TypedDicts.

For example, the following generic TypedDict can (probably) be defined now:

T = TypeVar('T', bound=Union[int, float, complex])

class Point(TypedDict, Generic[T]):
    x: T
    y: T

int_point: Point[int] = {"x": 1, "y": 2}
float_point: Point[float] = {"x": 1.0, "y": 2.0}
complex_point: Point[complex] = {"x": 1j, "y": 2j}

It would be nice if trycast() recognized parameterized TypedDict types like Point[int]

Confusing error message when arguments in trycast functions are swapped

Consider the following MRE where within the print function, I've swapped the argument position in the trycast function:

# script.py

from trycast import trycast
from typing import TypedDict


class Person(TypedDict):
    name: str
    age: float


p: Person = {"name": "mike", "age": 61.0}

print(trycast(p, Person))

Here, the first argument of the trycast function is the object (instead of the type) and the second argument is the type (instead of the object). It invokes the following error:

Traceback (most recent call last):
  File "/home/rednafi/workspace/personal/demo/script.py", line 12, in <module>
    print(trycast(p, Person))
  File "/home/rednafi/workspace/personal/demo/.venv/lib/python3.9/site-packages/trycast.py", line 232, in trycast
    if isinstance(value, type):  # type: ignore
TypeError: isinstance() arg 2 must be a type or tuple of types

IMHO, this is a leaky abstraction and the error should be handled in a graceful manner. To fix this, we can take some inspiration from the typing.cast function. If we swap the argument position in the typing.cast function, mypy throws the following error:

# script.py
from typing import cast
from typing import TypedDict

class Person(TypedDict):
    name: str
    age: float


p : Person = {'name': "hello", "age": 11.0}

print(cast(p, Person))               # Notice we're using `typing.cast` instead of `trycast`
script.py:14: error: Variable "script.p" is not valid as a type

This error is concise and less confusing.

Test Bench:
Python Version: 3.9.5
Operating System: Ubuntu 20.04

Callable[[Any * N], Any]: Trouble matching against constructors for certain built-in types

In the test suite's test_callable_p_r you will see lines like:

# NOTE: Cannot introspect constructor for certain built-in types
# TODO: Define __signature__ for failing internal types
self.assertTryCastFailure(Callable[[], Any], bool)
self.assertTryCastFailure(Callable[[], Any], int)
self.assertTryCastSuccess(Callable[[], Any], float)
self.assertTryCastSuccess(Callable[[], Any], complex)
self.assertTryCastFailure(Callable[[], Any], str)
self.assertTryCastSuccess(Callable[[], Any], list)
self.assertTryCastFailure(Callable[[], Any], dict)
self.assertTryCastSuccess(Callable[[], Any], tuple)

We want all of those lines to all be assertTryCastSuccess (and still have the test pass).

The failing lines above occur because an inspect.Signature cannot be generated for the constructor of certain built-in types:

inspect.signature(bool)  # ERROR: ValueError: no signature found for builtin type
inspect.signature(int)  # ERROR: ValueError: no signature found for builtin type
inspect.signature(float)  # OK
inspect.signature(complex)  # OK
inspect.signature(str)  # ERROR: ValueError: no signature found for builtin type
inspect.signature(list)  # OK
inspect.signature(dict)  # ERROR: ValueError: no signature found for builtin type
inspect.signature(tuple)  # OK

Based on reading the implementation of inspect.signature() and the underlying _signature_from_callable(), it looks like the desired fix would be to define an __signature__ on the type itself. For example:

sig = inspect.Signature()
bool.__signature__ = sig  # ERROR: TypeError: can't set attributes of built-in/extension type 'bool'
assert inspect.signature(bool) == sig

Unfortunately as the ERROR above alludes, there's no way to set __signature__ directly from normal Python code.

Next steps:

  • Find/create issue on upstream Python issue tracker RE inspect.signature(bool) (and calls on similar built-in types) not working
  • Shepherd that issue to resolution

Recognize Required and NotRequired items, defined by PEP 655

PEP 655 was accepted in Python 3.11. It introduces new typing qualifiers Required[] and NotRequired[] which trycast will need to be able to recognize.

Until this support is added, I expect trycast() and isassignable() will always return False (or crash) when given a TypedDict type that includes those new qualifiers.

no license

your pypi page suggests MIT license, but here there is absolutely nothing. So legally it is not possible to use this library.

typing(_extensions).TypedDict: Fix to treat as open by default

trycast() was originally designed to recognize mypy_extensions.TypedDict (which is closed) and therefore assumes that all TypedDict instances are closed by default. This is incorrect when working with a typing.TypedDict or typing_extensions.TypedDict (which is open). Please fix trycast() to recognize the latter types as open correctly.

Background & History

The original mypy_extensions.TypedDict was closed by default: A typechecker would not allow you to add unrecognized keys to such an instance.

However the later-implemented typing.TypedDict and typing_extensions.TypedDict are open by default: A typechecker WILL allow you to add unrecognized keys to such an instance. (Typecheckers will however complain if you add unrecognized keys in a TypedDict literal.)

Appendix: Other Libraries

FWIW, all other notable runtime type checking libraries that recognize TypedDicts also incorrectly treat them as closed at the time of writing:

  • runtype – runtype.validation.isa(value, T)

Also there was considerable confusion on the typing-sig mailing list about the default open vs. closed state of TypedDicts recently:

Ability to parse JSON with arbitrary keys

Is it possible to enforce the presence of certain keys but ignore all others? My use case is parsing JSON payloads from an event bus where I want to ignore fields that may be present but that I don't care to include in the TypedDict.

This functionality would be similar to json-schema's additionalProperties: true keyword.

For example:

from typing import TypedDict, NotRequired
from trycast import trycast

class MyType(TypedDict):
    a: str
    b: NotRequired[str | None]

# currently fails validation because "c" is in the dict
trycast(MyType, {"a": "foo", "b": "bar", "c": "baz"})

eval_type_str: Consider add to public API

eval_type_str is an internal function of the trycast module that is used to convert a stringified type annotation (like "typing.List") to a type annotation object (like typing.List).

Why expose?

  • This function was mentioned prominently in a PyCon US 2022 Typing Summit presentation. The cat is already out of the bag.
  • Trycast would be the only runtime type checker to expose this kind of functionality (either privately or publicly) to my knowledge. So this function would be a clear differentiating feature.

Tasks

  • Draft (extensive) tests that probe the eval_type_str function, and its limitations
  • Add function to API Reference (& Changelog)

Enforce positional-only parameters for public functions

Several functions in the API Reference are documented as having positional-only parameters but in fact are not declared with those parameters as positional-only in the source code because positional-only parameters require Python 3.8 and trycast currently supports Python 3.7+.

Please declare these functions with actual positional-only parameters once support for Python 3.7 is dropped - scheduled for 27 Jun 2023 - and only Python 3.8+ support remains.

Proposed alignment with `typing-validation` library

Hi David 👋,

I have only today found out about this project via the related TypeForm PEP discussion, which I would like to bring forward and for which I have written to you separately.

For the past few years, I have been using and maintaining typing-validation, a small library which performs runtime typechecking for a variety of rich types. Given that typing-validation is very aligned in spirit to trycast—as well as fairly aligned in API, save for the choice to raise TypeError—I would be keen to migrate to trycast at some point in the future and deprecate typing-validation in our internal code.

A few questions, if you don't mind, to gauge the extent to which this might be possible:

  1. I presume you'd be open to PRs which merely add support for additional types, in the style of your current code. Please let me know if you have additional requirements, and/or whether you're open to some (ideally light) code restructuring.
  2. I have extensively tested typing-validation over the years. Would you be open to PRs including additional tests, including for types which are already supported?
  3. The core functionality of typing-validation is implemented by a validate function which raises type error (rather than a type guard like isassignable). Would you be open to such a function being included in trycast? The advantage is that the raised TypeErrors can carry additional information about the specific error causes.
  4. Independently of 3, would you be open to having a dedicated function to inspect types and build structured typechecking information? This is implemented in typing-validation by passing an inspector object to validate, but the logic could easily be extracted from there and reused across the library. It produces detailed, structured type errors, as well as information as to why types cannot currently validated, and isassignable, trycast, validate and can_validate could all be built on top of it.

Points 3 and 4 are related to #16, and can immediately contribute a lot of tested code to it.

PS: The current code for typing-validation is a little messy 😳, but it works and a lot of our internal stuff depends on it for correctness, so I've been reticent to change it. It would be cleaned up in contributions to this project.

Support PEP 655: Required[] and NotRequired[] in TypedDict

PEP 655 introduces the Required[] and NotRequired[] special forms. These are not currently understood by trycast: trycast will never successfully cast to a TypedDict type that uses Required[] or NotRequired[].

Please fix trycast to understand Required[] and NotRequired[], from both:

  • the typing_extensions module and
  • the Python 3.11 typing module.

Support Never

The Never type was introduced in Python 3.11. Trycast should recognize it in the same places that NoReturn is accepted.

Drop support for mypy_extensions.TypedDict

Why:

  • This kind of TypedDict was introduced in the era of Python 3.7, which is no longer supported by trycast.
    • It is reasonable (in @davidfstr 's opinion) to encourage other users to migrate to a version of TypedDict from the standard library (Python 3.9+) or from typing_extensions (Python 3.8).
  • Extra code is required to support this very old kind of TypedDict.

Timing:

  • Suggest actually dropping mypy_extensions.TypedDict support at the same time as dropping support for Python 3.8, because then the trycast(strict=...) parameter (which involves both Python 3.8's TypedDict and mypy_extensions.TypedDict, and nothing else) can be cleanly removed simplified at the same time

Recognize ReadOnly items, defined by PEP 705

PEP 705 was recently accepted for Python 3.13. It introduces a new typing qualifier ReadOnly[] which trycast will need to be able to recognize.

Until this support is added, I expect trycast() and isassignable() will always return False (or crash) when given a TypedDict type that includes ReadOnly items.

checkcast: Provide information as to why a type is not "assignable"

Ive been using trycast for a while for runtime type checking (mostly yaml/json -> TypedDict) and its working great (thanks)

Mostly i use a pattern of checking if something is assignable and raising a TypeCastingError with the failing value to allow handling further up the stack

What would be really helpful is to be able to expose why the object is not assignable (eg "incorrect key", "incorrect type for key", "missing key" etc)

i guess there is some complexity around issues with different types of Type

implementing this might also add some complexity around continuing after the first failure and collecting the errors etc

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.