Skip to content

Commit 166be68

Browse files
authored
feat: allow explicit variable bound to be passed to Name (#241)
* feat: allow explicit variable bound to be passed to Name * remove docstring * bump
1 parent 1ac9973 commit 166be68

File tree

5 files changed

+72
-30
lines changed

5 files changed

+72
-30
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ ci:
55

66
repos:
77
- repo: https://github.com/crate-ci/typos
8-
rev: v1
8+
rev: v1.31.1
99
hooks:
1010
- id: typos
1111
args: []
1212

1313
- repo: https://github.com/astral-sh/ruff-pre-commit
14-
rev: v0.11.4
14+
rev: v0.11.7
1515
hooks:
1616
- id: ruff
1717
args: ["--fix", "--unsafe-fixes"]

src/app_model/expressions/_context_keys.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ def __init__(
9797
*,
9898
id: str = "", # optional because of __set_name__
9999
) -> None:
100-
super().__init__(id or "")
100+
bound = type(default_value) if default_value is not MISSING else None
101+
super().__init__(id or "", bound=bound)
101102
self._default_value = default_value
102103
self._getter = getter
103104
self._description = description

src/app_model/expressions/_expressions.py

Lines changed: 63 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,21 @@
3030

3131
from pydantic.annotated_handlers import GetCoreSchemaHandler
3232
from pydantic_core import core_schema
33+
from typing_extensions import TypedDict, Unpack
3334

3435
from ._context_keys import ContextKey
3536

37+
# Used for node end positions in constructor keyword arguments
38+
_EndPositionT = TypeVar("_EndPositionT", int, None)
39+
40+
# Corresponds to the names in the `_attributes`
41+
# class variable which is non-empty in certain AST nodes
42+
class _Attributes(TypedDict, Generic[_EndPositionT], total=False):
43+
lineno: int
44+
col_offset: int
45+
end_lineno: _EndPositionT
46+
end_col_offset: _EndPositionT
47+
3648

3749
def parse_expression(expr: Expr | str) -> Expr:
3850
"""Parse string expression into an [`Expr`][app_model.expressions.Expr] instance.
@@ -326,7 +338,7 @@ def __invert__(self) -> UnaryOp[T]:
326338
return UnaryOp(ast.Not(), self)
327339

328340
def __reduce_ex__(self, protocol: SupportsIndex) -> tuple[Any, ...]:
329-
rv = list(super().__reduce_ex__(protocol))
341+
rv: list[Any] = list(super().__reduce_ex__(protocol))
330342
rv[1] = tuple(getattr(self, f) for f in self._fields)
331343
return tuple(rv)
332344

@@ -368,24 +380,45 @@ def _iter_names(self) -> Iterator[str]:
368380
class Name(Expr[T], ast.Name):
369381
"""A variable name.
370382
371-
`id` holds the name as a string.
383+
Parameters
384+
----------
385+
id : str
386+
The name of the variable.
387+
bound : Any | None
388+
The type of the variable represented by this name (i.e. the type to which this
389+
name evaluates to when used in an expression). This is used to provide type
390+
hints when evaluating the expression. If `None`, the type is not known.
372391
"""
373392

374-
def __init__(self, id: str, ctx: ast.expr_context = LOAD, **kwargs: Any) -> None:
375-
kwargs["ctx"] = LOAD
376-
super().__init__(id, **kwargs)
393+
def __init__(
394+
self,
395+
id: str,
396+
ctx: ast.expr_context = LOAD,
397+
*,
398+
bound: type[T] | None = None,
399+
**kwargs: Unpack[_Attributes],
400+
) -> None:
401+
super().__init__(id, ctx=ctx, **kwargs)
402+
self.bound = bound
377403

378404

379405
class Constant(Expr[V], ast.Constant):
380406
"""A constant value.
381407
382-
The `value` attribute contains the Python object it represents.
383-
types supported: NoneType, str, bytes, bool, int, float
408+
Parameters
409+
----------
410+
value : V
411+
the Python object this constant represents.
412+
Types supported: NoneType, str, bytes, bool, int, float
413+
kind : str | None
414+
The kind of constant. This is used to provide type hints when
384415
"""
385416

386417
value: V
387418

388-
def __init__(self, value: V, kind: str | None = None, **kwargs: Any) -> None:
419+
def __init__(
420+
self, value: V, kind: str | None = None, **kwargs: Unpack[_Attributes]
421+
) -> None:
389422
_valid_type = (type(None), str, bytes, bool, int, float)
390423
if not isinstance(value, _valid_type):
391424
raise TypeError(f"Constants must be type: {_valid_type!r}")
@@ -405,13 +438,10 @@ def __init__(
405438
left: Expr,
406439
ops: Sequence[ast.cmpop],
407440
comparators: Sequence[Expr],
408-
**kwargs: Any,
441+
**kwargs: Unpack[_Attributes],
409442
) -> None:
410443
super().__init__(
411-
Expr._cast(left),
412-
ops,
413-
[Expr._cast(c) for c in comparators],
414-
**kwargs,
444+
Expr._cast(left), ops, [Expr._cast(c) for c in comparators], **kwargs
415445
)
416446

417447

@@ -426,9 +456,9 @@ def __init__(
426456
left: T | Expr[T],
427457
op: ast.operator,
428458
right: T | Expr[T],
429-
**k: Any,
459+
**kwargs: Unpack[_Attributes],
430460
) -> None:
431-
super().__init__(Expr._cast(left), op, Expr._cast(right), **k)
461+
super().__init__(Expr._cast(left), op, Expr._cast(right), **kwargs)
432462

433463

434464
class BoolOp(Expr[T], ast.BoolOp):
@@ -445,7 +475,7 @@ def __init__(
445475
self,
446476
op: ast.boolop,
447477
values: Sequence[ConstType | Expr],
448-
**kwargs: Any,
478+
**kwargs: Unpack[_Attributes],
449479
):
450480
super().__init__(op, [Expr._cast(v) for v in values], **kwargs)
451481

@@ -456,7 +486,9 @@ class UnaryOp(Expr[T], ast.UnaryOp):
456486
`op` is the operator, and `operand` any expression node.
457487
"""
458488

459-
def __init__(self, op: ast.unaryop, operand: Expr, **kwargs: Any) -> None:
489+
def __init__(
490+
self, op: ast.unaryop, operand: Expr, **kwargs: Unpack[_Attributes]
491+
) -> None:
460492
super().__init__(op, Expr._cast(operand), **kwargs)
461493

462494

@@ -466,7 +498,9 @@ class IfExp(Expr, ast.IfExp):
466498
`body` if `test` else `orelse`
467499
"""
468500

469-
def __init__(self, test: Expr, body: Expr, orelse: Expr, **kwargs: Any) -> None:
501+
def __init__(
502+
self, test: Expr, body: Expr, orelse: Expr, **kwargs: Unpack[_Attributes]
503+
) -> None:
470504
super().__init__(
471505
Expr._cast(test), Expr._cast(body), Expr._cast(orelse), **kwargs
472506
)
@@ -479,10 +513,12 @@ class Tuple(Expr, ast.Tuple):
479513
"""
480514

481515
def __init__(
482-
self, elts: Sequence[Expr], ctx: ast.expr_context = LOAD, **kwargs: Any
516+
self,
517+
elts: Sequence[Expr],
518+
ctx: ast.expr_context = LOAD,
519+
**kwargs: Unpack[_Attributes],
483520
) -> None:
484-
kwargs["ctx"] = ctx
485-
super().__init__(elts=[Expr._cast(e) for e in elts], **kwargs)
521+
super().__init__(elts=[Expr._cast(e) for e in elts], ctx=ctx, **kwargs)
486522

487523

488524
class List(Expr, ast.List):
@@ -492,10 +528,12 @@ class List(Expr, ast.List):
492528
"""
493529

494530
def __init__(
495-
self, elts: Sequence[Expr], ctx: ast.expr_context = LOAD, **kwargs: Any
531+
self,
532+
elts: Sequence[Expr],
533+
ctx: ast.expr_context = LOAD,
534+
**kwargs: Unpack[_Attributes],
496535
) -> None:
497-
kwargs["ctx"] = ctx
498-
super().__init__(elts=[Expr._cast(e) for e in elts], **kwargs)
536+
super().__init__(elts=[Expr._cast(e) for e in elts], ctx=ctx, **kwargs)
499537

500538

501539
class Set(Expr, ast.Set):
@@ -504,7 +542,7 @@ class Set(Expr, ast.Set):
504542
`elts` is a list of expressions.
505543
"""
506544

507-
def __init__(self, elts: Sequence[Expr], **kwargs: Any) -> None:
545+
def __init__(self, elts: Sequence[Expr], **kwargs: Unpack[_Attributes]) -> None:
508546
super().__init__(elts=[Expr._cast(e) for e in elts], **kwargs)
509547

510548

src/app_model/types/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
from ._menu_rule import MenuItem, MenuItemBase, MenuRule, SubmenuItem
2020

2121
if TYPE_CHECKING:
22-
from typing import Callable, TypeAlias
22+
from typing import Callable
23+
24+
from typing_extensions import TypeAlias
2325

2426
from ._icon import IconOrDict as IconOrDict
2527
from ._keybinding_rule import KeyBindingRuleDict as KeyBindingRuleDict

tests/test_context/test_expressions.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88

99

1010
def test_names():
11-
assert Name("n").eval({"n": 5}) == 5
11+
expr = Name("n", bound=int)
12+
assert expr.eval({"n": 5}) == 5
1213

1314
# currently, evaludating with a missing name is an error.
1415
with pytest.raises(NameError):

0 commit comments

Comments
 (0)