Skip to content

Commit

Permalink
feat: Initial pass of complex types for unmarshalling.
Browse files Browse the repository at this point in the history
  • Loading branch information
seandstewart committed Jun 19, 2024
1 parent 1c6aa1c commit 82b566c
Show file tree
Hide file tree
Showing 7 changed files with 546 additions and 90 deletions.
13 changes: 12 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ inflection = "^0.5"
pendulum = "^3"
orjson = {version = "^3", optional = true}
typing-extensions = {version = "^4.10"}
more-itertools = "^10.2.0"

[tool.poetry.group.test.dependencies]
pytest = "^8"
Expand Down Expand Up @@ -112,8 +113,6 @@ extend-select = [
# Future annotation
"FA"
]
[tool.ruff.lint.mccabe]
max-complexity = 15

[tool.ruff.lint.per-file-ignores]
# Ignore `E402` (import violations) in all `__init__.py` files
Expand Down
3 changes: 1 addition & 2 deletions src/typelib/future.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import functools
import sys
import typing
from ast import unparse

__all__ = ("transform_annotation",)

Expand All @@ -26,7 +25,7 @@ def transform(annotation: str, *, union: str = "Union") -> str:
"""
parsed = ast.parse(annotation, mode="eval")
transformed = TransformUnion().generic_visit(parsed)
unparsed = unparse(transformed).strip()
unparsed = ast.unparse(transformed).strip()
return unparsed


Expand Down
113 changes: 108 additions & 5 deletions src/typelib/inspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
import datetime
import decimal
import enum
import fractions
import functools
import inspect
import ipaddress
import numbers
import pathlib
import re
import sqlite3
import types
import typing
Expand All @@ -39,14 +41,15 @@
overload,
)

from typelib import compat, contrib, refs
from typelib import compat, constants, contrib, refs

__all__ = (
"BUILTIN_TYPES",
"isabstract",
"isbuiltininstance",
"isbuiltintype",
"isbuiltinsubtype",
"isbytestype",
"isclassvartype",
"iscollectiontype",
"isdatetype",
Expand All @@ -70,13 +73,15 @@
"isstdlibinstance",
"isstdlibtype",
"isstdlibsubtype",
"isstringtype",
"istexttype",
"istimetype",
"istimedeltatype",
"istupletype",
"istypeddict",
"istypedtuple",
"isuniontype",
"isunresolvable",
"isuuidtype",
"should_unwrap",
)
Expand All @@ -101,7 +106,7 @@ def origin(annotation: Any) -> Any:
>>> class Foo: ...
...
>>> origin(Foo)
<class 'typelib.Foo'>
<class 'typelib.Foo'>
"""
# Resolve custom NewTypes.
actual = resolve_supertype(annotation)
Expand Down Expand Up @@ -164,14 +169,14 @@ def get_args(annotation: Any) -> Tuple[Any, ...]:
Examples:
>>> from typelib import inspection
>>> from typing import Dict, TypeVar
>>> from typing import Dict, TypeVar, Any
>>> T = TypeVar("T")
>>> get_args(Dict)
()
>>> get_args(Dict[str, int])
(<class 'str'>, <class 'int'>)
>>> get_args(Dict[str, T])
(<class 'str'>,)
(<class 'str'>, typing.Any)
"""
args = typing.get_args(annotation)
if not args:
Expand Down Expand Up @@ -681,6 +686,22 @@ def isdecimaltype(obj: type) -> compat.TypeIs[type[decimal.Decimal]]:
return builtins.issubclass(origin(obj), decimal.Decimal)


@compat.cache
def isfractiontype(obj: type) -> compat.TypeIs[type[fractions.Fraction]]:
"""Test whether this annotation is a Decimal object.
Examples:
>>> import fractions
>>> from typing import NewType
>>> isdecimaltype(fractions.Fraction)
True
>>> isdecimaltype(NewType("Foo", fractions.Fraction))
True
"""
return builtins.issubclass(origin(obj), fractions.Fraction)


@compat.cache
def isuuidtype(obj: type) -> compat.TypeIs[type[uuid.UUID]]:
"""Test whether this annotation is a a date/datetime object.
Expand Down Expand Up @@ -1167,7 +1188,35 @@ def istexttype(t: type[Any]) -> compat.TypeIs[type[str | bytes | bytearray]]:
>>> istexttype(MyStr)
True
"""
return issubclass(t, (str, bytes, bytearray))
return issubclass(t, (str, bytes, bytearray, memoryview))


@compat.cache
def isstringtype(t: type[Any]) -> compat.TypeIs[type[str | bytes | bytearray]]:
"""Test whether the given type is a subclass of text or bytes.
Examples:
>>> class MyStr(str): ...
...
>>> istexttype(MyStr)
True
"""
return issubclass(t, str)


@compat.cache
def isbytestype(t: type[Any]) -> compat.TypeIs[type[str | bytes | bytearray]]:
"""Test whether the given type is a subclass of text or bytes.
Examples:
>>> class MyStr(str): ...
...
>>> istexttype(MyStr)
True
"""
return issubclass(t, (bytes, bytearray))


@compat.cache
Expand Down Expand Up @@ -1274,9 +1323,63 @@ def issubscriptedgeneric(t: Any) -> bool:

@compat.cache # type: ignore[arg-type]
def iscallable(t: Any) -> compat.TypeIs[Callable]:
"""Test whether the given type is a callable.
Examples:
>>> import typing
>>> import collections.abc
>>> iscallable(lambda: None)
True
>>> iscallable(typing.Callable)
True
>>> iscallable(collections.abc.Callable)
True
>>> iscallable(1)
False
"""
return inspect.isroutine(t) or t is Callable or _safe_issubclass(t, abc_Callable) # type: ignore[arg-type]


@compat.cache
def isunresolvable(t: Any) -> bool:
"""Test whether the given type is unresolvable.
Examples:
>>> import typing
>>> isunresolvable(int)
False
>>> isunresolvable(typing.Any)
True
>>> isunresolvable(...)
True
"""
return t in _UNRESOLVABLE


_UNRESOLVABLE = (
Any,
re.Match,
type(None),
None,
constants.empty,
Callable,
abc_Callable,
inspect.Parameter.empty,
type(Ellipsis),
Ellipsis,
)


@compat.cache
def ispatterntype(t: Any) -> compat.TypeIs[re.Pattern]:
return issubclass(t, re.Pattern)


@compat.cache
def ispathtype(t: Any) -> compat.TypeIs[pathlib.Path]:
return issubclass(t, pathlib.PurePath)


def _safe_issubclass(__cls: type, __class_or_tuple: type | tuple[type, ...]) -> bool:
try:
return issubclass(__cls, __class_or_tuple)
Expand Down
Loading

0 comments on commit 82b566c

Please sign in to comment.