Skip to content

Commit

Permalink
feat: Unmarshallers for initial primitive types.
Browse files Browse the repository at this point in the history
  • Loading branch information
seandstewart committed Jun 19, 2024
1 parent 108faa1 commit 1c6aa1c
Show file tree
Hide file tree
Showing 9 changed files with 725 additions and 67 deletions.
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,15 @@ 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
"__init__.py" = ["E402"]

[tool.ruff.lint.isort]
extra-standard-library = ["typing_extensions"]
extra-standard-library = ["typing_extensions", "graphlib"]

[tool.mypy]
mypy_path = "$MYPY_CONFIG_FILE_DIR/src/"
Expand Down
14 changes: 11 additions & 3 deletions src/typelib/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"Self",
"TypeGuard",
"TypeIs",
"TypeVarTuple",
"DATACLASS_KW_ONLY",
"DATACLASS_MATCH_ARGS",
"DATACLASS_NATIVE_SLOTS",
Expand All @@ -26,7 +27,14 @@
DATACLASS_MATCH_ARGS: Final[bool] = True
KW_ONLY: Final[object] = object()

from typing_extensions import TypeIs, ParamSpec, Self, Final
from typing_extensions import (
TypeIs,
ParamSpec,
Self,
Final,
TypeVarTuple,
TypeGuard,
)

import orjson as json

Expand Down Expand Up @@ -54,10 +62,10 @@ def cache(func: F) -> F: ...
from typing_extensions import TypeIs

if sys.version_info >= (3, 11):
from typing import ParamSpec, Self, Final
from typing import ParamSpec, Self, Final, TypeVarTuple

else:
from typing_extensions import ParamSpec, Self, Final
from typing_extensions import ParamSpec, Self, Final, TypeVarTuple

if sys.version_info >= (3, 10):
from typing import TypeGuard
Expand Down
10 changes: 4 additions & 6 deletions src/typelib/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@

import collections
import dataclasses
import functools
import graphlib
import inspect
import typing

import graphlib

from typelib import classes, constants, inspection, refs
from typelib import classes, compat, constants, inspection, refs

__all__ = ("itertypes",)
__all__ = ("static_order", "itertypes", "get_type_graph")


@functools.cache
@compat.cache
def static_order(t: type | str | refs.ForwardRef) -> typing.Iterable[TypeNode]:
"""Get an ordered iterable of types which resolve into the root type provided.
Expand Down
33 changes: 28 additions & 5 deletions src/typelib/inspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"isdescriptor",
"isenumtype",
"isfinal",
"isfixedtuple",
"isfixedtupletype",
"isforwardref",
"isfromdictclass",
"isfrozendataclass",
Expand Down Expand Up @@ -794,6 +794,29 @@ def iscollectiontype(obj: type) -> compat.TypeIs[type[Collection]]:
_COLLECTIONS = {list, set, tuple, frozenset, dict, str, bytes}


@compat.cache
def issubscriptedcollectiontype(
obj: type[Generic[_ArgsT]], # type: ignore[valid-type]
) -> compat.TypeIs[type[Collection[_ArgsT]]]:
"""Test whether this annotation is a collection type and is subscripted.
Examples:
>>> from typing import Collection, Mapping, NewType
>>> issubscriptedcollectiontype(Collection)
False
>>> issubscriptedcollectiontype(Mapping[str, str])
True
>>> issubscriptedcollectiontype(str)
False
>>> issubscriptedcollectiontype(NewType("Foo", Collection[int]))
True
"""
return iscollectiontype(obj) and issubscriptedgeneric(obj)


_ArgsT = TypeVar("_ArgsT")


@compat.cache
def ismappingtype(obj: type) -> compat.TypeIs[type[Mapping]]:
"""Test whether this annotation is a subtype of :py:class:`typing.Mapping`.
Expand Down Expand Up @@ -974,17 +997,17 @@ def isnamedtuple(obj: type) -> compat.TypeIs[type[NamedTuple]]:


@compat.cache
def isfixedtuple(obj: type) -> compat.TypeIs[type[tuple]]:
def isfixedtupletype(obj: type) -> compat.TypeIs[type[tuple]]:
"""Check whether an object is a "fixed" tuple, e.g., tuple[int, int].
Examples:
>>> from typing import Tuple
>>>
>>>
>>> isfixedtuple(Tuple[str, int])
>>> isfixedtupletype(Tuple[str, int])
True
>>> isfixedtuple(Tuple[str, ...])
>>> isfixedtupletype(Tuple[str, ...])
False
"""
args = get_args(obj)
Expand Down Expand Up @@ -1191,7 +1214,7 @@ def isstructuredtype(t: type[Any]) -> bool:
False
"""
return (
isfixedtuple(t)
isfixedtupletype(t)
or isnamedtuple(t)
or istypeddict(t)
or (not isstdlibsubtype(origin(t)) and not isuniontype(t) and not isliteral(t))
Expand Down
141 changes: 141 additions & 0 deletions src/typelib/interchange.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
from __future__ import annotations

import datetime
import time
import typing as t

import pendulum

from typelib import compat


@t.overload
def decode(val: bytes | bytearray | memoryview, *, encoding: str = "utf8") -> str: ... # type: ignore[overload-overlap]


@t.overload
def decode(val: _T) -> _T: ... # type: ignore[overload-overlap]


def decode(val: t.Any, *, encoding: str = "utf8") -> t.Any:
"""Decode a bytes-like object into a str.
Notes:
If a non-bytes-like object is passed, it will be returned unchanged.
Args:
val: The object to be decoded.
encoding: The encoding to use when decoding the object (defaults "utf8").
"""
val = val.tobytes() if isinstance(val, memoryview) else val
if isinstance(val, (bytes, bytearray)):
decoded = val.decode(encoding)
return decoded
return val


@compat.lru_cache(maxsize=100_000)
def isoformat(t: datetime.date | datetime.time | datetime.timedelta) -> str:
"""Format any date/time object into an ISO-8601 string.
Notes:
While the standard library includes `isoformat()` methods for
:py:class:`datetime.date`, :py:class:`datetime.time`, &
:py:class:`datetime.datetime`, they do not include a method for serializing
:py:class:`datetime.timedelta`, even though durations are included in the
ISO 8601 specification. This function fills that gap.
Examples:
>>> import datetime
>>> from typelib import interchange
>>> interchange.isoformat(datetime.date(1970, 1, 1))
'1970-01-01'
>>> interchange.isoformat(datetime.time())
'00:00:00'
>>> interchange.isoformat(datetime.datetime(1970, 1, 1))
'1970-01-01T00:00:00'
>>> interchange.isoformat(datetime.timedelta())
'P0Y0M0DT0H0M0.000000S'
"""
if isinstance(t, (datetime.date, datetime.time)):
return t.isoformat()
d: pendulum.Duration = (
t
if isinstance(t, pendulum.Duration)
else pendulum.duration(
days=t.days,
seconds=t.seconds,
microseconds=t.microseconds,
)
)
period = (
f"P{d.years}Y{d.months}M{d.remaining_days}D"
f"T{d.hours}H{d.minutes}M{d.remaining_seconds}.{d.microseconds:06}S"
)
return period


_T = t.TypeVar("_T")


def unixtime(t: datetime.date | datetime.time) -> float:
"""Convert a date/time object to a unix timestamp.
Args:
t: The object to be converted.
"""
if isinstance(t, datetime.time):
t = datetime.datetime.now(tz=t.tzinfo).replace(
hour=t.hour,
minute=t.minute,
second=t.second,
microsecond=t.microsecond,
)

return time.mktime(t.timetuple())


DateTimeT = t.TypeVar("DateTimeT", datetime.date, datetime.time, datetime.timedelta)


@compat.lru_cache(maxsize=100_000)
def dateparse(val: str, t: type[DateTimeT]) -> DateTimeT:
"""Parse a date string into a datetime object.
Args:
val: The date string to parse.
t: The target datetime type.
Returns:
The parsed datetime object.
"""
try:
parsed: pendulum.DateTime | pendulum.Duration = pendulum.parse(val) # type: ignore[assignment]
if isinstance(parsed, pendulum.DateTime):
if issubclass(t, datetime.time):
return parsed.time().replace(tzinfo=parsed.tzinfo)
if issubclass(t, datetime.datetime):
return parsed
if issubclass(t, datetime.date):
return parsed.date()
if not isinstance(parsed, t):
raise ValueError(f"Cannot parse {val!r} as {t.__qualname__!r}")
return parsed
except ValueError:
if val.isdigit() or val.isdecimal():
numval = float(val)
# Assume the number value is seconds - same logic as time-since-epoch
if issubclass(t, datetime.timedelta):
return datetime.timedelta(seconds=numval)
# Parse a datetime from the time-since-epoch as indicated by the value.
dt = datetime.datetime.fromtimestamp(numval, tz=datetime.timezone.utc)
# Return the datetime if the target type is a datetime
if issubclass(t, datetime.datetime):
return dt
# If the target type is a time object, just return the time.
if issubclass(t, datetime.time):
return dt.time().replace(tzinfo=dt.tzinfo)
# If the target type is a date object, just return the date.
return dt.date()

raise
Loading

0 comments on commit 1c6aa1c

Please sign in to comment.