Skip to content

Commit a8bee11

Browse files
committed
Improve the state of Python type hints in basilisp.lang.*
1 parent 049f53d commit a8bee11

File tree

10 files changed

+107
-73
lines changed

10 files changed

+107
-73
lines changed

CHANGELOG.md

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

3434
### Other
35-
* Improve the state of the Python type hints in `basilisp.lang.*` (#797)
35+
* Improve the state of the Python type hints in `basilisp.lang.*` (#797, #784)
3636

3737

3838
## [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: 55 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@
2424
Optional,
2525
Pattern,
2626
Tuple,
27+
TypeVar,
2728
Union,
2829
cast,
2930
)
3031

3132
import attr
33+
from typing_extensions import Concatenate, ParamSpec
3234

3335
from basilisp.lang import keyword as kw
3436
from basilisp.lang import list as llist
@@ -91,9 +93,9 @@
9193
PyTuple,
9294
)
9395
from basilisp.lang.compiler.nodes import Queue as QueueNode
94-
from basilisp.lang.compiler.nodes import Quote, ReaderLispForm, Recur, Reify, Require
96+
from basilisp.lang.compiler.nodes import Quote, Recur, Reify, Require
9597
from basilisp.lang.compiler.nodes import Set as SetNode
96-
from basilisp.lang.compiler.nodes import SetBang, Throw, Try, VarRef
98+
from basilisp.lang.compiler.nodes import SetBang, T_withmeta, Throw, Try, VarRef
9799
from basilisp.lang.compiler.nodes import Vector as VectorNode
98100
from basilisp.lang.compiler.nodes import WithMeta, Yield
99101
from basilisp.lang.interfaces import IMeta, IRecord, ISeq, ISeqable, IType
@@ -329,8 +331,11 @@ def reduce(*genned: "GeneratedPyAST") -> "GeneratedPyAST":
329331

330332

331333
PyASTStream = Iterable[ast.AST]
332-
SimplePyASTGenerator = Callable[[GeneratorContext, ReaderLispForm], GeneratedPyAST]
333-
PyASTGenerator = Callable[[GeneratorContext, Node], GeneratedPyAST]
334+
T_node = TypeVar("T_node", bound=Node)
335+
P_generator = ParamSpec("P_generator")
336+
PyASTGenerator = Callable[
337+
Concatenate[GeneratorContext, T_node, P_generator], GeneratedPyAST
338+
]
334339

335340

336341
####################
@@ -366,12 +371,19 @@ def attr_node(node, idx):
366371
return attr_node(ast.Name(id=attrs[0], ctx=ast.Load()), 1)
367372

368373

369-
def _simple_ast_generator(gen_ast):
374+
P_simplegen = ParamSpec("P_simplegen")
375+
376+
377+
def _simple_ast_generator(
378+
gen_ast: Callable[P_simplegen, ast.AST]
379+
) -> Callable[P_simplegen, GeneratedPyAST]:
370380
"""Wrap simpler AST generators to return a GeneratedPyAST."""
371381

372382
@wraps(gen_ast)
373-
def wrapped_ast_generator(ctx: GeneratorContext, form: LispForm) -> GeneratedPyAST:
374-
return GeneratedPyAST(node=gen_ast(ctx, form))
383+
def wrapped_ast_generator(
384+
*args: P_simplegen.args, **kwargs: P_simplegen.kwargs
385+
) -> GeneratedPyAST:
386+
return GeneratedPyAST(node=gen_ast(*args, **kwargs))
375387

376388
return wrapped_ast_generator
377389

@@ -519,23 +531,30 @@ def _ast_with_loc(
519531
return py_ast
520532

521533

522-
def _with_ast_loc(f):
534+
def _with_ast_loc(
535+
f: PyASTGenerator[T_node, P_generator]
536+
) -> PyASTGenerator[T_node, P_generator]:
523537
"""Wrap a generator function in a decorator to supply line and column
524538
information to the returned Python AST node. Dependency nodes will not
525539
be hydrated, functions whose returns need dependency nodes to be
526540
hydrated should use `_with_ast_loc_deps` below."""
527541

528542
@wraps(f)
529543
def with_lineno_and_col(
530-
ctx: GeneratorContext, node: Node, *args, **kwargs
544+
ctx: GeneratorContext,
545+
node: T_node,
546+
*args: P_generator.args,
547+
**kwargs: P_generator.kwargs,
531548
) -> GeneratedPyAST:
532549
py_ast = f(ctx, node, *args, **kwargs)
533550
return _ast_with_loc(py_ast, node.env)
534551

535552
return with_lineno_and_col
536553

537554

538-
def _with_ast_loc_deps(f):
555+
def _with_ast_loc_deps(
556+
f: PyASTGenerator[T_node, P_generator]
557+
) -> PyASTGenerator[T_node, P_generator]:
539558
"""Wrap a generator function in a decorator to supply line and column
540559
information to the returned Python AST node and dependency nodes.
541560
@@ -546,7 +565,10 @@ def _with_ast_loc_deps(f):
546565

547566
@wraps(f)
548567
def with_lineno_and_col(
549-
ctx: GeneratorContext, node: Node, *args, **kwargs
568+
ctx: GeneratorContext,
569+
node: T_node,
570+
*args: P_generator.args,
571+
**kwargs: P_generator.kwargs,
550572
) -> GeneratedPyAST:
551573
py_ast = f(ctx, node, *args, **kwargs)
552574
return _ast_with_loc(py_ast, node.env, include_dependencies=True)
@@ -841,16 +863,15 @@ def _def_to_py_ast( # pylint: disable=too-many-locals
841863
assert node.init is not None # silence MyPy
842864
if node.init.op == NodeOp.FN:
843865
assert isinstance(node.init, Fn)
844-
def_ast = _fn_to_py_ast( # type: ignore[call-arg]
845-
ctx, node.init, def_name=defsym.name
846-
)
866+
def_ast = _fn_to_py_ast(ctx, node.init, def_name=defsym.name)
847867
is_defn = True
848868
elif (
849869
node.init.op == NodeOp.WITH_META
850870
and isinstance(node.init, WithMeta)
851871
and node.init.expr.op == NodeOp.FN
852872
):
853873
assert isinstance(node.init, WithMeta)
874+
assert isinstance(node.init.expr, Fn)
854875
def_ast = _with_meta_to_py_ast(ctx, node.init, def_name=defsym.name)
855876
is_defn = True
856877
else:
@@ -1309,8 +1330,8 @@ def __deftype_staticmethod_to_py_ast(
13091330
)
13101331

13111332

1312-
DefTypeASTGenerator = Callable[[GeneratorContext, DefTypeMember], GeneratedPyAST]
1313-
_DEFTYPE_MEMBER_HANDLER: Mapping[NodeOp, DefTypeASTGenerator] = {
1333+
T_deftypenode = TypeVar("T_deftypenode", bound=DefTypeMember)
1334+
_DEFTYPE_MEMBER_HANDLER: Mapping[NodeOp, PyASTGenerator] = {
13141335
NodeOp.DEFTYPE_CLASSMETHOD: __deftype_classmethod_to_py_ast,
13151336
NodeOp.DEFTYPE_METHOD: __deftype_method_to_py_ast,
13161337
NodeOp.DEFTYPE_PROPERTY: __deftype_property_to_py_ast,
@@ -1320,7 +1341,7 @@ def __deftype_staticmethod_to_py_ast(
13201341

13211342
def __deftype_member_to_py_ast(
13221343
ctx: GeneratorContext,
1323-
node: DefTypeMember,
1344+
node: T_deftypenode,
13241345
) -> GeneratedPyAST:
13251346
member_type = node.op
13261347
handle_deftype_member = _DEFTYPE_MEMBER_HANDLER.get(member_type)
@@ -1477,7 +1498,9 @@ def _deftype_to_py_ast( # pylint: disable=too-many-locals
14771498
)
14781499

14791500

1480-
def _wrap_override_var_indirection(f: PyASTGenerator) -> PyASTGenerator:
1501+
def _wrap_override_var_indirection(
1502+
f: PyASTGenerator[T_node, P_generator]
1503+
) -> PyASTGenerator[T_node, P_generator]:
14811504
"""
14821505
Wrap a Node generator to apply a special override requiring Var indirection
14831506
for any Var accesses generated within top-level `do` blocks.
@@ -1496,11 +1519,14 @@ def _wrap_override_var_indirection(f: PyASTGenerator) -> PyASTGenerator:
14961519

14971520
@wraps(f)
14981521
def _wrapped_do(
1499-
ctx: GeneratorContext, node: Node, *args, **kwargs
1522+
ctx: GeneratorContext,
1523+
node: T_node,
1524+
*args: P_generator.args,
1525+
**kwargs: P_generator.kwargs,
15001526
) -> GeneratedPyAST:
15011527
if isinstance(node, Do) and node.top_level:
15021528
with ctx.with_var_indirection_override():
1503-
return f(ctx, node, *args, **kwargs)
1529+
return f(ctx, cast(T_node, node), *args, **kwargs)
15041530
else:
15051531
with ctx.with_var_indirection_override(False):
15061532
return f(ctx, node, *args, **kwargs)
@@ -3370,7 +3396,7 @@ def _py_tuple_to_py_ast(ctx: GeneratorContext, node: PyTuple) -> GeneratedPyAST:
33703396
############
33713397

33723398

3373-
_WITH_META_EXPR_HANDLER = {
3399+
_WITH_META_EXPR_HANDLER: Mapping[NodeOp, PyASTGenerator] = {
33743400
NodeOp.FN: _fn_to_py_ast,
33753401
NodeOp.MAP: _map_to_py_ast,
33763402
NodeOp.QUEUE: _queue_to_py_ast,
@@ -3381,7 +3407,10 @@ def _py_tuple_to_py_ast(ctx: GeneratorContext, node: PyTuple) -> GeneratedPyAST:
33813407

33823408

33833409
def _with_meta_to_py_ast(
3384-
ctx: GeneratorContext, node: WithMeta, **kwargs
3410+
ctx: GeneratorContext,
3411+
node: WithMeta[T_withmeta],
3412+
*args: P_generator.args,
3413+
**kwargs: P_generator.kwargs,
33853414
) -> GeneratedPyAST:
33863415
"""Generate a Python AST node for Python interop method calls."""
33873416
assert node.op == NodeOp.WITH_META
@@ -3390,7 +3419,7 @@ def _with_meta_to_py_ast(
33903419
assert (
33913420
handle_expr is not None
33923421
), "No expression handler for with-meta child node type"
3393-
return handle_expr(ctx, node.expr, meta_node=node.meta, **kwargs)
3422+
return handle_expr(ctx, node.expr, meta_node=node.meta, *args, **kwargs)
33943423

33953424

33963425
#################
@@ -3635,7 +3664,7 @@ def _const_record_to_py_ast(form: IRecord, ctx: GeneratorContext) -> GeneratedPy
36353664
key_nodes = _kw_to_py_ast(k, ctx)
36363665
keys.append(key_nodes.node)
36373666
assert (
3638-
len(key_nodes.dependencies) == 0
3667+
not key_nodes.dependencies
36393668
), "Simple AST generators must emit no dependencies"
36403669

36413670
val_nodes = _const_val_to_py_ast(v, ctx)
@@ -3756,7 +3785,7 @@ def _const_node_to_py_ast(ctx: GeneratorContext, lisp_ast: Const) -> GeneratedPy
37563785
NodeOp.PY_TUPLE: _py_tuple_to_py_ast,
37573786
NodeOp.QUEUE: _queue_to_py_ast,
37583787
NodeOp.QUOTE: _quote_to_py_ast,
3759-
NodeOp.RECUR: _recur_to_py_ast, # type: ignore
3788+
NodeOp.RECUR: _recur_to_py_ast,
37603789
NodeOp.REIFY: _reify_to_py_ast,
37613790
NodeOp.REQUIRE: _require_to_py_ast,
37623791
NodeOp.SET: _set_to_py_ast,
@@ -3766,7 +3795,7 @@ def _const_node_to_py_ast(ctx: GeneratorContext, lisp_ast: Const) -> GeneratedPy
37663795
NodeOp.YIELD: _yield_to_py_ast,
37673796
NodeOp.VAR: _var_sym_to_py_ast,
37683797
NodeOp.VECTOR: _vec_to_py_ast,
3769-
NodeOp.WITH_META: _with_meta_to_py_ast, # type: ignore
3798+
NodeOp.WITH_META: _with_meta_to_py_ast,
37703799
}
37713800

37723801

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)