Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions packages/typemap/src/typemap/type_eval/_eval_operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
Bool,
Capitalize,
DropAnnotations,
DeepPartial,
FromUnion,
GenericCallable,
GetAnnotations,
Expand All @@ -36,6 +37,7 @@
IsAssignable,
IsEquivalent,
Iter,
KeyOf,
Length,
Lowercase,
Member,
Expand All @@ -47,6 +49,7 @@
Slice,
SpecialFormEllipsis,
StrConcat,
Template,
Uncapitalize,
UpdateClass,
Uppercase,
Expand Down Expand Up @@ -1138,6 +1141,27 @@ def _eval_Length(tp, *, ctx) -> typing.Any:
raise TypeError(f"Invalid type argument to Length: {tp} is not a tuple")


@type_eval.register_evaluator(KeyOf)
@_lift_over_unions
def _eval_KeyOf(tp, *, ctx):
"""Evaluate KeyOf[T] to get all member names as a tuple of Literals."""
tp = _eval_types(tp, ctx)
hints = get_annotated_type_hints(
tp, include_extras=True, attrs_only=True, ctx=ctx
)

if not hints:
return typing.Literal[()]

# Extract member names and create tuple of Literal types
names = []
for name in hints:
names.append(typing.Literal[name])

# Return as tuple of Literal types (use unpacking to make it hashable)
return tuple[*names] # type: ignore[return-value]


@type_eval.register_evaluator(Slice)
@_lift_over_unions
def _eval_Slice(tp, start, end, *, ctx):
Expand Down Expand Up @@ -1174,6 +1198,37 @@ def func(*args, ctx):
_string_literal_op(StrConcat, op=lambda s, t: s + t)


@type_eval.register_evaluator(Template)
def _eval_Template(*parts, ctx):
"""Evaluate Template to concatenate all string parts."""
evaluated_parts = []
for part in parts:
evaled = _eval_types(part, ctx)
if _typing_inspect.is_generic_alias(evaled):
if evaled.__origin__ is typing.Literal:
# Extract literal string value
lit_val = evaled.__args__[0]
if isinstance(lit_val, str):
evaluated_parts.append(lit_val)
else:
raise TypeError(
f"Template parts must be string literals, got {lit_val}"
)
else:
raise TypeError(
f"Template parts must be string literals, got {evaled}"
)
elif isinstance(evaled, str):
# Plain string (shouldn't happen but handle it)
evaluated_parts.append(evaled)
else:
raise TypeError(
f"Template parts must be string literals, got {type(evaled)}"
)

return typing.Literal["".join(evaluated_parts)]


##################################################################


Expand All @@ -1198,6 +1253,75 @@ def _eval_RaiseError(msg, *extra_types, ctx):
raise TypeMapError(message)


@type_eval.register_evaluator(DeepPartial)
def _eval_DeepPartial(tp, *, ctx):
"""Evaluate DeepPartial[T] to create a class with all fields optional.

For each field in T:
- If it's a primitive type, make it optional (T | None)
- If it's a complex type (class with attrs), recursively apply DeepPartial
- Create a new class with all optional fields
"""
from typing import get_args

tp = _eval_types(tp, ctx)

# Get attributes using Attrs to get Member objects
attrs_result = _eval_Attrs(tp, ctx=ctx)
attrs_args = get_args(attrs_result)

if not attrs_args:
# No fields, return the original type
return tp

new_annotations = {}

for member in attrs_args:
# Get the member name and type
# member.name is an associated type, need to evaluate it
name_result = _eval_types(member.name, ctx)
name = (
get_args(name_result)[0]
if hasattr(name_result, "__args__")
else name_result
)

# member.type is an associated type, need to evaluate it
type_result = _eval_types(member.type, ctx)

# Check if this is a complex type (class with its own attributes)
if isinstance(type_result, type):
# Try to get attrs of this type, but don't recurse if it fails
# (nested types may not be in evaluation context)
try:
nested_attrs = _eval_Attrs(type_result, ctx=ctx)
nested_args = get_args(nested_attrs)
if nested_args:
# Has nested fields - try to recursively apply
# but catch any errors (e.g., NameError for undefined types)
try:
nested_partial = _eval_DeepPartial(type_result, ctx=ctx)
new_annotations[name] = nested_partial | None
except NameError, TypeError:
# If recursion fails, just make optional
new_annotations[name] = type_result | None
else:
# No nested fields, just make optional
new_annotations[name] = type_result | None
except NameError, TypeError, AttributeError:
# If we can't inspect it, just make optional
new_annotations[name] = type_result | None
else:
# Primitive or unknown type, just make optional
new_annotations[name] = type_result | None

# Create a new class with optional fields
class_name = (
f"DeepPartial_{tp.__name__ if hasattr(tp, '__name__') else 'Anonymous'}"
)
return type(class_name, (), {"__annotations__": new_annotations})


##################################################################


Expand Down
42 changes: 42 additions & 0 deletions packages/typemap/src/typemap/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,19 @@ class Length[S: tuple]:
pass


class KeyOf[T]:
"""Return all member names as a tuple.

Usage:
type Keys = KeyOf[User]
# Returns tuple[Literal['name'], Literal['email'], Literal['age']]

This is similar to TypeScript's keyof operator.
"""

pass


class Slice[S: str | tuple, Start: int | None, End: int | None](
_TupleLikeOperator
):
Expand All @@ -288,6 +301,19 @@ class StrConcat[S: str, T: str]:
pass


class Template[*Parts]:
"""Template literal string builder.

Usage:
type Route = Template['/', Resource, '/id']
# For Resource = 'users', returns: '/users/id'

This allows building string templates from parts that get concatenated.
"""

pass


class NewProtocol[*T]:
pass

Expand All @@ -309,6 +335,22 @@ class RaiseError[S: str, *Ts]:
pass


class DeepPartial[T]:
"""Make all fields recursively optional.

For each field in T:
- If it's a primitive type (int, str, etc.), make it optional
- If it's a complex type (class with attrs), recursively apply DeepPartial

Usage:
type DeepUser = DeepPartial[User]

Note: This creates a new class at runtime with all fields optional.
"""

pass


##################################################################

# TODO: type better
Expand Down
Loading