Skip to content

Commit 51d0de2

Browse files
committed
Improve the state of Python type hints in basilisp.lang.*
1 parent 1bb2e8f commit 51d0de2

File tree

10 files changed

+109
-78
lines changed

10 files changed

+109
-78
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3333
* Removed support for PyPy 3.8 (#785)
3434

3535
### Other
36-
* Improve the state of the Python type hints in `basilisp.lang.*` (#797)
36+
* Improve the state of the Python type hints in `basilisp.lang.*` (#797, #784)
3737

3838

3939
## [v0.1.0b0]

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ prompt-toolkit = "^3.0.0"
3737
pyrsistent = "^0.18.0"
3838
python-dateutil = "^2.8.1"
3939
readerwriterlock = "^1.0.8"
40+
typing_extensions = "^4.9.0"
4041

4142
astor = { version = "^0.8.1", python = "<3.9", optional = true }
4243
pytest = { version = "^7.0.0", optional = true }
@@ -217,6 +218,7 @@ disable = [
217218

218219
[tool.mypy]
219220
check_untyped_defs = true
221+
disallow_untyped_decorators = true
220222
mypy_path = "src/"
221223
show_error_codes = true
222224
warn_redundant_casts = true

src/basilisp/lang/atom.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from typing import Callable, Generic, Optional, TypeVar
22

33
from readerwriterlock.rwlock import RWLockFair
4+
from typing_extensions import Concatenate, ParamSpec
45

56
from basilisp.lang.interfaces import IPersistentMap, RefValidator
67
from basilisp.lang.map import PersistentMap
78
from basilisp.lang.reference import RefBase
89

910
T = TypeVar("T")
11+
P = ParamSpec("P")
1012

1113

1214
class Atom(RefBase[T], Generic[T]):
@@ -58,7 +60,9 @@ def reset(self, v: T) -> T:
5860
self._notify_watches(oldval, v)
5961
return v
6062

61-
def swap(self, f: Callable[..., T], *args, **kwargs) -> T:
63+
def swap(
64+
self, f: Callable[Concatenate[T, P], T], *args: P.args, **kwargs: P.kwargs
65+
) -> T:
6266
"""Atomically swap the state of the Atom to the return value of
6367
`f(old, *args, **kwargs)`, returning the new value."""
6468
while True:

src/basilisp/lang/compiler/analyzer.py

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
Pattern,
2929
Set,
3030
Tuple,
31+
TypeVar,
3132
Union,
3233
cast,
3334
)
@@ -669,12 +670,17 @@ def _loc(form: Union[LispForm, ISeq]) -> Optional[Tuple[int, int, int, int]]:
669670
return None
670671

671672

672-
def _with_loc(f):
673+
T_form = TypeVar("T_form", bound=LispForm)
674+
T_node = TypeVar("T_node", Node, None)
675+
LispAnalyzer = Callable[[T_form, AnalyzerContext], T_node]
676+
677+
678+
def _with_loc(f: LispAnalyzer) -> LispAnalyzer:
673679
"""Attach any available location information from the input form to
674680
the node environment returned from the parsing function."""
675681

676682
@wraps(f)
677-
def _analyze_form(form: Union[LispForm, ISeq], ctx: AnalyzerContext) -> Node:
683+
def _analyze_form(form: T_form, ctx: AnalyzerContext) -> Node:
678684
form_loc = _loc(form)
679685
if form_loc is None:
680686
return f(form, ctx)
@@ -795,24 +801,15 @@ def _tag_ast(form: Optional[LispForm], ctx: AnalyzerContext) -> Optional[Node]:
795801
return _analyze_form(form, ctx)
796802

797803

798-
def _with_meta(gen_node):
804+
def _with_meta(gen_node: LispAnalyzer) -> LispAnalyzer:
799805
"""Wraps the node generated by gen_node in a :with-meta AST node if the
800806
original form has meta.
801807
802808
:with-meta AST nodes are used for non-quoted collection literals and for
803809
function expressions."""
804810

805811
@wraps(gen_node)
806-
def with_meta(
807-
form: Union[
808-
llist.PersistentList,
809-
lmap.PersistentMap,
810-
ISeq,
811-
lset.PersistentSet,
812-
vec.PersistentVector,
813-
],
814-
ctx: AnalyzerContext,
815-
) -> Node:
812+
def with_meta(form: T_form, ctx: AnalyzerContext) -> Node:
816813
assert not ctx.is_quoted, "with-meta nodes are not used in quoted expressions"
817814

818815
descriptor = gen_node(form, ctx)
@@ -826,7 +823,7 @@ def with_meta(
826823
isinstance(meta_ast, Const) and meta_ast.type == ConstType.MAP
827824
)
828825
return WithMeta(
829-
form=form,
826+
form=cast(T_form, form),
830827
meta=meta_ast,
831828
expr=descriptor,
832829
env=ctx.get_node_env(pos=ctx.syntax_position),
@@ -3113,7 +3110,7 @@ def _yield_ast(form: ISeq, ctx: AnalyzerContext) -> Yield:
31133110
return Yield.expressionless(form, ctx.get_node_env(pos=ctx.syntax_position))
31143111

31153112

3116-
SpecialFormHandler = Callable[[ISeq, AnalyzerContext], SpecialFormNode]
3113+
SpecialFormHandler = Callable[[T_form, AnalyzerContext], SpecialFormNode]
31173114
_SPECIAL_FORM_HANDLERS: Mapping[sym.Symbol, SpecialFormHandler] = {
31183115
SpecialForm.AWAIT: _await_ast,
31193116
SpecialForm.DEF: _def_ast,

src/basilisp/lang/compiler/generator.py

Lines changed: 57 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
)
3232

3333
import attr
34+
from typing_extensions import Concatenate, ParamSpec
3435

3536
from basilisp.lang import keyword as kw
3637
from basilisp.lang import list as llist
@@ -93,9 +94,9 @@
9394
PyTuple,
9495
)
9596
from basilisp.lang.compiler.nodes import Queue as QueueNode
96-
from basilisp.lang.compiler.nodes import Quote, ReaderLispForm, Recur, Reify, Require
97+
from basilisp.lang.compiler.nodes import Quote, Recur, Reify, Require
9798
from basilisp.lang.compiler.nodes import Set as SetNode
98-
from basilisp.lang.compiler.nodes import SetBang, Throw, Try, VarRef
99+
from basilisp.lang.compiler.nodes import SetBang, T_withmeta, Throw, Try, VarRef
99100
from basilisp.lang.compiler.nodes import Vector as VectorNode
100101
from basilisp.lang.compiler.nodes import WithMeta, Yield
101102
from basilisp.lang.interfaces import IMeta, IRecord, ISeq, ISeqable, IType
@@ -332,8 +333,11 @@ def reduce(*genned: "GeneratedPyAST[T_pynode]") -> "GeneratedPyAST[T_pynode]":
332333

333334

334335
PyASTStream = Iterable[ast.AST]
335-
SimplePyASTGenerator = Callable[[GeneratorContext, ReaderLispForm], GeneratedPyAST]
336-
PyASTGenerator = Callable[[GeneratorContext, Node], GeneratedPyAST]
336+
T_node = TypeVar("T_node", bound=Node)
337+
P_generator = ParamSpec("P_generator")
338+
PyASTGenerator = Callable[
339+
Concatenate[GeneratorContext, T_node, P_generator], GeneratedPyAST[T_pynode]
340+
]
337341

338342

339343
####################
@@ -369,12 +373,19 @@ def attr_node(node, idx):
369373
return attr_node(ast.Name(id=attrs[0], ctx=ast.Load()), 1)
370374

371375

372-
def _simple_ast_generator(gen_ast):
376+
P_simplegen = ParamSpec("P_simplegen")
377+
378+
379+
def _simple_ast_generator(
380+
gen_ast: Callable[P_simplegen, ast.AST]
381+
) -> Callable[P_simplegen, GeneratedPyAST]:
373382
"""Wrap simpler AST generators to return a GeneratedPyAST."""
374383

375384
@wraps(gen_ast)
376-
def wrapped_ast_generator(ctx: GeneratorContext, form: LispForm) -> GeneratedPyAST:
377-
return GeneratedPyAST(node=gen_ast(ctx, form))
385+
def wrapped_ast_generator(
386+
*args: P_simplegen.args, **kwargs: P_simplegen.kwargs
387+
) -> GeneratedPyAST:
388+
return GeneratedPyAST(node=gen_ast(*args, **kwargs))
378389

379390
return wrapped_ast_generator
380391

@@ -524,23 +535,30 @@ def _ast_with_loc(
524535
return py_ast
525536

526537

527-
def _with_ast_loc(f):
538+
def _with_ast_loc(
539+
f: PyASTGenerator[T_node, P_generator, T_pynode]
540+
) -> PyASTGenerator[T_node, P_generator, T_pynode]:
528541
"""Wrap a generator function in a decorator to supply line and column
529542
information to the returned Python AST node. Dependency nodes will not
530543
be hydrated, functions whose returns need dependency nodes to be
531544
hydrated should use `_with_ast_loc_deps` below."""
532545

533546
@wraps(f)
534547
def with_lineno_and_col(
535-
ctx: GeneratorContext, node: Node, *args, **kwargs
536-
) -> GeneratedPyAST:
548+
ctx: GeneratorContext,
549+
node: T_node,
550+
*args: P_generator.args,
551+
**kwargs: P_generator.kwargs,
552+
) -> GeneratedPyAST[T_pynode]:
537553
py_ast = f(ctx, node, *args, **kwargs)
538554
return _ast_with_loc(py_ast, node.env)
539555

540556
return with_lineno_and_col
541557

542558

543-
def _with_ast_loc_deps(f):
559+
def _with_ast_loc_deps(
560+
f: PyASTGenerator[T_node, P_generator, T_pynode]
561+
) -> PyASTGenerator[T_node, P_generator, T_pynode]:
544562
"""Wrap a generator function in a decorator to supply line and column
545563
information to the returned Python AST node and dependency nodes.
546564
@@ -551,8 +569,11 @@ def _with_ast_loc_deps(f):
551569

552570
@wraps(f)
553571
def with_lineno_and_col(
554-
ctx: GeneratorContext, node: Node, *args, **kwargs
555-
) -> GeneratedPyAST:
572+
ctx: GeneratorContext,
573+
node: T_node,
574+
*args: P_generator.args,
575+
**kwargs: P_generator.kwargs,
576+
) -> GeneratedPyAST[T_pynode]:
556577
py_ast = f(ctx, node, *args, **kwargs)
557578
return _ast_with_loc(py_ast, node.env, include_dependencies=True)
558579

@@ -847,16 +868,15 @@ def _def_to_py_ast( # pylint: disable=too-many-locals
847868
assert node.init is not None # silence MyPy
848869
if node.init.op == NodeOp.FN:
849870
assert isinstance(node.init, Fn)
850-
def_ast = _fn_to_py_ast( # type: ignore[call-arg]
851-
ctx, node.init, def_name=defsym.name
852-
)
871+
def_ast = _fn_to_py_ast(ctx, node.init, def_name=defsym.name)
853872
is_defn = True
854873
elif (
855874
node.init.op == NodeOp.WITH_META
856875
and isinstance(node.init, WithMeta)
857876
and node.init.expr.op == NodeOp.FN
858877
):
859878
assert isinstance(node.init, WithMeta)
879+
assert isinstance(node.init.expr, Fn)
860880
def_ast = _with_meta_to_py_ast(ctx, node.init, def_name=defsym.name)
861881
is_defn = True
862882
else:
@@ -1315,10 +1335,8 @@ def __deftype_staticmethod_to_py_ast(
13151335
)
13161336

13171337

1318-
DefTypeASTGenerator = Callable[
1319-
[GeneratorContext, DefTypeMember], GeneratedPyAST[ast.stmt]
1320-
]
1321-
_DEFTYPE_MEMBER_HANDLER: Mapping[NodeOp, DefTypeASTGenerator] = {
1338+
T_deftypenode = TypeVar("T_deftypenode", bound=DefTypeMember)
1339+
_DEFTYPE_MEMBER_HANDLER: Mapping[NodeOp, PyASTGenerator] = {
13221340
NodeOp.DEFTYPE_CLASSMETHOD: __deftype_classmethod_to_py_ast,
13231341
NodeOp.DEFTYPE_METHOD: __deftype_method_to_py_ast,
13241342
NodeOp.DEFTYPE_PROPERTY: __deftype_property_to_py_ast,
@@ -1328,7 +1346,7 @@ def __deftype_staticmethod_to_py_ast(
13281346

13291347
def __deftype_member_to_py_ast(
13301348
ctx: GeneratorContext,
1331-
node: DefTypeMember,
1349+
node: T_deftypenode,
13321350
) -> GeneratedPyAST[ast.stmt]:
13331351
member_type = node.op
13341352
handle_deftype_member = _DEFTYPE_MEMBER_HANDLER.get(member_type)
@@ -1485,7 +1503,9 @@ def _deftype_to_py_ast( # pylint: disable=too-many-locals
14851503
)
14861504

14871505

1488-
def _wrap_override_var_indirection(f: PyASTGenerator) -> PyASTGenerator:
1506+
def _wrap_override_var_indirection(
1507+
f: PyASTGenerator[T_node, P_generator, T_pynode]
1508+
) -> PyASTGenerator[T_node, P_generator, T_pynode]:
14891509
"""
14901510
Wrap a Node generator to apply a special override requiring Var indirection
14911511
for any Var accesses generated within top-level `do` blocks.
@@ -1504,11 +1524,14 @@ def _wrap_override_var_indirection(f: PyASTGenerator) -> PyASTGenerator:
15041524

15051525
@wraps(f)
15061526
def _wrapped_do(
1507-
ctx: GeneratorContext, node: Node, *args, **kwargs
1508-
) -> GeneratedPyAST:
1527+
ctx: GeneratorContext,
1528+
node: T_node,
1529+
*args: P_generator.args,
1530+
**kwargs: P_generator.kwargs,
1531+
) -> GeneratedPyAST[T_pynode]:
15091532
if isinstance(node, Do) and node.top_level:
15101533
with ctx.with_var_indirection_override():
1511-
return f(ctx, node, *args, **kwargs)
1534+
return f(ctx, cast(T_node, node), *args, **kwargs)
15121535
else:
15131536
with ctx.with_var_indirection_override(False):
15141537
return f(ctx, node, *args, **kwargs)
@@ -3358,7 +3381,7 @@ def _py_tuple_to_py_ast(
33583381
############
33593382

33603383

3361-
_WITH_META_EXPR_HANDLER = {
3384+
_WITH_META_EXPR_HANDLER: Mapping[NodeOp, PyASTGenerator] = {
33623385
NodeOp.FN: _fn_to_py_ast,
33633386
NodeOp.MAP: _map_to_py_ast,
33643387
NodeOp.QUEUE: _queue_to_py_ast,
@@ -3369,7 +3392,10 @@ def _py_tuple_to_py_ast(
33693392

33703393

33713394
def _with_meta_to_py_ast(
3372-
ctx: GeneratorContext, node: WithMeta, **kwargs
3395+
ctx: GeneratorContext,
3396+
node: WithMeta[T_withmeta],
3397+
*args: P_generator.args,
3398+
**kwargs: P_generator.kwargs,
33733399
) -> GeneratedPyAST[ast.expr]:
33743400
"""Generate a Python AST node for Python interop method calls."""
33753401
assert node.op == NodeOp.WITH_META
@@ -3378,7 +3404,7 @@ def _with_meta_to_py_ast(
33783404
assert (
33793405
handle_expr is not None
33803406
), "No expression handler for with-meta child node type"
3381-
return handle_expr(ctx, node.expr, meta_node=node.meta, **kwargs)
3407+
return handle_expr(ctx, node.expr, meta_node=node.meta, *args, **kwargs)
33823408

33833409

33843410
#################
@@ -3634,7 +3660,7 @@ def _const_record_to_py_ast(
36343660
key_nodes = _kw_to_py_ast(k, ctx)
36353661
keys.append(key_nodes.node)
36363662
assert (
3637-
len(key_nodes.dependencies) == 0
3663+
not key_nodes.dependencies
36383664
), "Simple AST generators must emit no dependencies"
36393665

36403666
val_nodes = _const_val_to_py_ast(v, ctx)
@@ -3759,7 +3785,7 @@ def _const_node_to_py_ast(
37593785
NodeOp.PY_TUPLE: _py_tuple_to_py_ast,
37603786
NodeOp.QUEUE: _queue_to_py_ast,
37613787
NodeOp.QUOTE: _quote_to_py_ast,
3762-
NodeOp.RECUR: _recur_to_py_ast, # type: ignore
3788+
NodeOp.RECUR: _recur_to_py_ast,
37633789
NodeOp.REIFY: _reify_to_py_ast,
37643790
NodeOp.REQUIRE: _require_to_py_ast,
37653791
NodeOp.SET: _set_to_py_ast,
@@ -3769,7 +3795,7 @@ def _const_node_to_py_ast(
37693795
NodeOp.YIELD: _yield_to_py_ast,
37703796
NodeOp.VAR: _var_sym_to_py_ast,
37713797
NodeOp.VECTOR: _vec_to_py_ast,
3772-
NodeOp.WITH_META: _with_meta_to_py_ast, # type: ignore
3798+
NodeOp.WITH_META: _with_meta_to_py_ast,
37733799
}
37743800

37753801

src/basilisp/lang/futures.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
from typing import Callable, Optional, TypeVar
66

77
import attr
8+
from typing_extensions import ParamSpec
89

910
from basilisp.lang.interfaces import IBlockingDeref
1011

1112
T = TypeVar("T")
13+
P = ParamSpec("P")
1214

1315

1416
@attr.frozen(eq=True, repr=False)
@@ -55,8 +57,8 @@ def __init__(self, max_workers: Optional[int] = None):
5557
super().__init__(max_workers=max_workers)
5658

5759
# pylint: disable=arguments-differ
58-
def submit( # type: ignore
59-
self, fn: Callable[..., T], *args, **kwargs
60+
def submit( # type: ignore[override]
61+
self, fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs
6062
) -> "Future[T]":
6163
return Future(super().submit(fn, *args, **kwargs))
6264

@@ -70,7 +72,7 @@ def __init__(
7072
super().__init__(max_workers=max_workers, thread_name_prefix=thread_name_prefix)
7173

7274
# pylint: disable=arguments-differ
73-
def submit( # type: ignore
74-
self, fn: Callable[..., T], *args, **kwargs
75+
def submit( # type: ignore[override]
76+
self, fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs
7577
) -> "Future[T]":
7678
return Future(super().submit(fn, *args, **kwargs))

0 commit comments

Comments
 (0)