Skip to content

Commit bf58828

Browse files
committed
Improve the state of Python type hints in basilisp.lang.*
1 parent f7da5a2 commit bf58828

26 files changed

+290
-246
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3030
### Removed
3131
* Removed support for PyPy 3.8 (#785)
3232

33+
### Other
34+
* Improve the state of the Python type hints in `basilisp.lang.*` (#???)
35+
36+
3337
## [v0.1.0b0]
3438
### Added
3539
* Added rudimentary support for `clojure.stacktrace` with `print-cause-trace` (part of #721)

pyproject.toml

Lines changed: 3 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,8 +218,10 @@ disable = [
217218

218219
[tool.mypy]
219220
check_untyped_defs = true
221+
disallow_untyped_decorators = true
220222
mypy_path = "src/"
221223
show_error_codes = true
224+
warn_redundant_casts = true
222225
warn_unused_configs = true
223226
warn_unused_ignores = true
224227

src/basilisp/cli.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,13 @@ def _subcommand(
237237
help: Optional[str] = None, # pylint: disable=redefined-builtin
238238
description: Optional[str] = None,
239239
handler: Handler,
240-
):
241-
def _wrap_add_subcommand(f: Callable[[argparse.ArgumentParser], None]):
240+
) -> Callable[
241+
[Callable[[argparse.ArgumentParser], None]],
242+
Callable[["argparse._SubParsersAction"], None],
243+
]:
244+
def _wrap_add_subcommand(
245+
f: Callable[[argparse.ArgumentParser], None]
246+
) -> Callable[["argparse._SubParsersAction"], None]:
242247
def _wrapped_subcommand(subparsers: "argparse._SubParsersAction"):
243248
parser = subparsers.add_parser(
244249
subcommand, help=help, description=description
@@ -279,14 +284,14 @@ def bootstrap_basilisp_installation(_, args: argparse.Namespace) -> None:
279284
description=textwrap.dedent(
280285
"""Bootstrap the Python installation to allow importing Basilisp namespaces"
281286
without requiring an additional bootstrapping step.
282-
287+
283288
Python installations are bootstrapped by installing a `basilispbootstrap.pth`
284289
file in your `site-packages` directory. Python installations execute `*.pth`
285290
files found at startup.
286-
291+
287292
Bootstrapping your Python installation in this way can help avoid needing to
288293
perform manual bootstrapping from Python code within your application.
289-
294+
290295
On the first startup, Basilisp will compile `basilisp.core` to byte code
291296
which could take up to 30 seconds in some cases depending on your system and
292297
which version of Python you are using. Subsequent startups should be
@@ -319,7 +324,7 @@ def _add_bootstrap_subcommand(parser: argparse.ArgumentParser) -> None:
319324
def nrepl_server(
320325
_,
321326
args: argparse.Namespace,
322-
):
327+
) -> None:
323328
opts = compiler.compiler_opts()
324329
basilisp.init(opts)
325330

@@ -369,7 +374,7 @@ def _add_nrepl_server_subcommand(parser: argparse.ArgumentParser) -> None:
369374
def repl(
370375
_,
371376
args: argparse.Namespace,
372-
):
377+
) -> None:
373378
opts = compiler.compiler_opts(
374379
warn_on_shadowed_name=args.warn_on_shadowed_name,
375380
warn_on_shadowed_var=args.warn_on_shadowed_var,
@@ -465,7 +470,7 @@ def _add_repl_subcommand(parser: argparse.ArgumentParser) -> None:
465470
def run(
466471
parser: argparse.ArgumentParser,
467472
args: argparse.Namespace,
468-
):
473+
) -> None:
469474
target = args.file_or_ns_or_code
470475
if args.load_namespace:
471476
if args.in_ns is not None:
@@ -523,18 +528,18 @@ def run(
523528
help="run a Basilisp script or code or namespace",
524529
description=textwrap.dedent(
525530
"""Run a Basilisp script or a line of code or load a Basilisp namespace.
526-
531+
527532
If `-c` is provided, execute the line of code as given. If `-n` is given,
528533
interpret `file_or_ns_or_code` as a fully qualified Basilisp namespace
529534
relative to `sys.path`. Otherwise, execute the file as a script relative to
530535
the current working directory.
531-
536+
532537
`*main-ns*` will be set to the value provided for `-n`. In all other cases,
533538
it will be `nil`."""
534539
),
535540
handler=run,
536541
)
537-
def _add_run_subcommand(parser: argparse.ArgumentParser):
542+
def _add_run_subcommand(parser: argparse.ArgumentParser) -> None:
538543
parser.add_argument(
539544
"file_or_ns_or_code",
540545
help=(
@@ -570,7 +575,9 @@ def _add_run_subcommand(parser: argparse.ArgumentParser):
570575
_add_debug_arg_group(parser)
571576

572577

573-
def test(parser: argparse.ArgumentParser, args: argparse.Namespace): # pragma: no cover
578+
def test(
579+
parser: argparse.ArgumentParser, args: argparse.Namespace
580+
) -> None: # pragma: no cover
574581
try:
575582
import pytest
576583
except (ImportError, ModuleNotFoundError):
@@ -591,7 +598,7 @@ def _add_test_subcommand(parser: argparse.ArgumentParser) -> None:
591598
parser.add_argument("args", nargs=-1)
592599

593600

594-
def version(_, __):
601+
def version(_, __) -> None:
595602
v = importlib.metadata.version("basilisp")
596603
print(f"Basilisp {v}")
597604

src/basilisp/importer.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,16 @@
88
from functools import lru_cache
99
from importlib.abc import MetaPathFinder, SourceLoader
1010
from importlib.machinery import ModuleSpec
11-
from typing import Iterable, List, Mapping, MutableMapping, Optional, Sequence, cast
11+
from typing import (
12+
Any,
13+
Iterable,
14+
List,
15+
Mapping,
16+
MutableMapping,
17+
Optional,
18+
Sequence,
19+
cast,
20+
)
1221

1322
from basilisp.lang import compiler as compiler
1423
from basilisp.lang import reader as reader
@@ -191,22 +200,22 @@ def find_spec(
191200
return ModuleSpec(fullname, None, is_package=True)
192201
return None
193202

194-
def invalidate_caches(self):
203+
def invalidate_caches(self) -> None:
195204
super().invalidate_caches()
196205
self._cache = {}
197206

198-
def _cache_bytecode(self, source_path, cache_path, data):
207+
def _cache_bytecode(self, source_path: str, cache_path: str, data: bytes) -> None:
199208
self.set_data(cache_path, data)
200209

201-
def path_stats(self, path):
210+
def path_stats(self, path: str) -> Mapping[str, Any]:
202211
stat = os.stat(path)
203212
return {"mtime": int(stat.st_mtime), "size": stat.st_size}
204213

205-
def get_data(self, path):
214+
def get_data(self, path: str) -> bytes:
206215
with open(path, mode="r+b") as f:
207216
return f.read()
208217

209-
def set_data(self, path, data):
218+
def set_data(self, path: str, data: bytes) -> None:
210219
os.makedirs(os.path.dirname(path), exist_ok=True)
211220
with open(path, mode="w+b") as f:
212221
f.write(data)
@@ -279,7 +288,7 @@ def get_code(self, fullname: str) -> Optional[types.CodeType]:
279288
assert len(code) == 1
280289
return code[0]
281290

282-
def create_module(self, spec: ModuleSpec):
291+
def create_module(self, spec: ModuleSpec) -> BasilispModule:
283292
logger.debug(f"Creating Basilisp module '{spec.name}'")
284293
mod = BasilispModule(spec.name)
285294
mod.__file__ = spec.loader_state["filename"]
@@ -400,7 +409,7 @@ def exec_module(self, module: types.ModuleType) -> None:
400409
self._exec_module(fullname, spec.loader_state, path_stats, ns)
401410

402411

403-
def hook_imports():
412+
def hook_imports() -> None:
404413
"""Hook into Python's import machinery with a custom Basilisp code
405414
importer.
406415

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: 15 additions & 18 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
)
@@ -175,13 +176,13 @@
175176
AnalyzerException = partial(CompilerException, phase=CompilerPhase.ANALYZING)
176177

177178

178-
@attr.s(auto_attribs=True, slots=True)
179+
@attr.define
179180
class RecurPoint:
180181
loop_id: str
181182
args: Collection[Binding] = ()
182183

183184

184-
@attr.s(auto_attribs=True, frozen=True, slots=True)
185+
@attr.frozen
185186
class SymbolTableEntry:
186187
binding: Binding
187188
used: bool = False
@@ -196,7 +197,7 @@ def context(self) -> LocalType:
196197
return self.binding.local
197198

198199

199-
@attr.s(auto_attribs=True, slots=True)
200+
@attr.define
200201
class SymbolTable:
201202
name: str
202203
_is_context_boundary: bool = False
@@ -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/exception.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class CompilerPhase(Enum):
3030
COMPILING_PYTHON = kw.keyword("compiling-python")
3131

3232

33-
@attr.s(auto_attribs=True, frozen=True, slots=True)
33+
@attr.frozen
3434
class _loc:
3535
line: Optional[int] = None
3636
col: Optional[int] = None
@@ -46,7 +46,7 @@ def __bool__(self):
4646
)
4747

4848

49-
@attr.s(auto_attribs=True, slots=True, str=False)
49+
@attr.define(str=False)
5050
class CompilerException(IExceptionInfo):
5151
msg: str
5252
phase: CompilerPhase

0 commit comments

Comments
 (0)