wyfo / apischema

JSON (de)serialization, GraphQL and JSON schema generation using Python typing.

Home Page:https://wyfo.github.io/apischema/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

ClassVar fails when __future__ annotations used in python3.9

thomascobb opened this issue · comments

from __future__ import annotations

from dataclasses import dataclass
from typing import ClassVar

from apischema.json_schema import deserialization_schema

@dataclass
class MyClass:
    a: ClassVar[str] = ""

def test_fails():
    assert deserialization_schema(MyClass)

Fails with:

./tests/test_classvar.py::test_fails Failed: [undefined]TypeError: typing.ClassVar[str] is not valid as type argument
Traceback (most recent call last):
  File "/home/tom/Programming/pvi/tests/test_classvar.py", line 15, in test_fails
    assert deserialization_schema(MyClass)
  File "/home/tom/.local/share/virtualenvs/pvi-YMoh_0sz/lib/python3.9/site-packages/apischema/utils.py", line 423, in wrapper
    return wrapped(*args, **kwargs)
  File "/home/tom/.local/share/virtualenvs/pvi-YMoh_0sz/lib/python3.9/site-packages/apischema/json_schema/schema.py", line 558, in deserialization_schema
    return _schema(
  File "/home/tom/.local/share/virtualenvs/pvi-YMoh_0sz/lib/python3.9/site-packages/apischema/json_schema/schema.py", line 517, in _schema
    refs = _extract_refs([(tp, conversion)], default_conversion, builder, all_refs)
  File "/home/tom/.local/share/virtualenvs/pvi-YMoh_0sz/lib/python3.9/site-packages/apischema/json_schema/schema.py", line 476, in _extract_refs
    builder.RefsExtractor(default_conversion, refs).visit_with_conv(tp, conversion)
  File "/home/tom/.local/share/virtualenvs/pvi-YMoh_0sz/lib/python3.9/site-packages/apischema/conversions/visitor.py", line 100, in visit_with_conv
    return self.visit(tp)
  File "/home/tom/.local/share/virtualenvs/pvi-YMoh_0sz/lib/python3.9/site-packages/apischema/conversions/visitor.py", line 135, in visit
    return self.visit_conversion(tp, conversion, dynamic, next_conversion)
  File "/home/tom/.local/share/virtualenvs/pvi-YMoh_0sz/lib/python3.9/site-packages/apischema/json_schema/refs.py", line 119, in visit_conversion
    for ref_tp in self.resolve_conversion(tp):
  File "/home/tom/.local/share/virtualenvs/pvi-YMoh_0sz/lib/python3.9/site-packages/apischema/json_schema/conversions_resolver.py", line 129, in resolve_conversion
    return Resolver(self.default_conversion).visit_with_conv(
  File "/home/tom/.local/share/virtualenvs/pvi-YMoh_0sz/lib/python3.9/site-packages/apischema/conversions/visitor.py", line 100, in visit_with_conv
    return self.visit(tp)
  File "/home/tom/.local/share/virtualenvs/pvi-YMoh_0sz/lib/python3.9/site-packages/apischema/conversions/visitor.py", line 135, in visit
    return self.visit_conversion(tp, conversion, dynamic, next_conversion)
  File "/home/tom/.local/share/virtualenvs/pvi-YMoh_0sz/lib/python3.9/site-packages/apischema/json_schema/conversions_resolver.py", line 97, in visit_conversion
    results = super().visit_conversion(
  File "/home/tom/.local/share/virtualenvs/pvi-YMoh_0sz/lib/python3.9/site-packages/apischema/conversions/visitor.py", line 122, in visit_conversion
    return super().visit(tp)
  File "/home/tom/.local/share/virtualenvs/pvi-YMoh_0sz/lib/python3.9/site-packages/apischema/visitor.py", line 185, in visit
    return self.dataclass(tp, *dataclass_types_and_fields(tp))  # type: ignore
  File "/home/tom/.local/share/virtualenvs/pvi-YMoh_0sz/lib/python3.9/site-packages/apischema/visitor.py", line 64, in dataclass_types_and_fields
    types = get_type_hints2(tp)
  File "/home/tom/.local/share/virtualenvs/pvi-YMoh_0sz/lib/python3.9/site-packages/apischema/typing.py", line 190, in get_type_hints2
    hints.update(_class_annotations(base, globalns, localns))
  File "/home/tom/.local/share/virtualenvs/pvi-YMoh_0sz/lib/python3.9/site-packages/apischema/typing.py", line 167, in _class_annotations
    hints[name] = _eval_type(value, base_globals, localns)
  File "/usr/lib/python3.9/typing.py", line 292, in _eval_type
    return t._evaluate(globalns, localns, recursive_guard)
  File "/usr/lib/python3.9/typing.py", line 553, in _evaluate
    type_ = _type_check(
  File "/usr/lib/python3.9/typing.py", line 158, in _type_check
    raise TypeError(f"{arg} is not valid as type argument")
TypeError: typing.ClassVar[str] is not valid as type argument

With apischema 0.16.4 on python 3.9

I had a quick dig around and it appears that ForwardRef added an is_class argument for python3.9 and fails for ClassVars if this is not passed.

I'm happy to submit a PR that adds yet another version check in apischema/typing.py if that's useful, but was curious as why you don't use get_type_hints() to get this information?

but was curious as why you don't use get_type_hints() to get this information?

Because typing.get_type_hints is not sufficient to retrieve type annotation of generic classes, particularly in case of inheritance, so I'd to made my own wrapper around it. You can look at python/typing#776 and python/typing#777, where I've tried to add better generic handling in standard library (without result).

Here is an example:

from typing import Generic, TypeVar, get_type_hints
from apischema.typing import get_type_hints2

T = TypeVar("T")

class A(Generic[T]):
    a: T

U = TypeVar("U")

class B(A[U]):
    b: list[U]

# How can I work with this result?
assert get_type_hints(B) == {"a": T, "b": list[U]}
# Better
assert get_type_hints2(B) == {"a": U, "b": list[U]}

However, it seems that I have to rewrite my wrapper, because it is using too much internal implementation details. I will try to release a patch for v0.15 and v0.16 this weekend.

Having written the fix, I remember now why I did a rewrite of get_type_hints. As you can see in python/typing#776, the goal was to propose a new implementation of get_type_hints, that's why I used a lot of internal features. But it was a mistake to do it, I should have kept this implementation for the typing issue, and make my own wrapper.

Thanks for the quick fix, much appreciated!