• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

basilisp-lang / basilisp / 7379247703

01 Jan 2024 06:37PM UTC coverage: 99.008%. Remained the same
7379247703

push

github

web-flow
callable var (#768)

Hi,

could you please review compatibility patch with Clojure to make vars
callable. It addresses #767.

I am not sure this if the `class Var` is the right place to make vars
callable or the analyzer, which should expand them to a callable var
value last node.

Nevertheless, I will kindly request your help with the type hinting,
currently it will fail the linter with the following error

```
src/basilisp/lang/runtime.py:279:15: E1102: self.value is not callable (not-callable)
```

but not sure how to fix it, I've tried the class Var `value` property
method to return a maybe Callable but it didn't work.

Thanks

Co-authored-by: ikappaki <ikappaki@users.noreply.github.com>

1691 of 1693 branches covered (0.0%)

Branch coverage included in aggregate %.

7892 of 7986 relevant lines covered (98.82%)

0.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.92
/src/basilisp/cli.py
1
import argparse
1✔
2
import importlib.metadata
1✔
3
import io
1✔
4
import os
1✔
5
import sys
1✔
6
import traceback
1✔
7
import types
1✔
8
from pathlib import Path
1✔
9
from typing import Any, Callable, Optional, Sequence, Type
1✔
10

11
from basilisp import main as basilisp
1✔
12
from basilisp.lang import compiler as compiler
1✔
13
from basilisp.lang import reader as reader
1✔
14
from basilisp.lang import runtime as runtime
1✔
15
from basilisp.lang import symbol as sym
1✔
16
from basilisp.prompt import get_prompter
1✔
17

18
CLI_INPUT_FILE_PATH = "<CLI Input>"
1✔
19
REPL_INPUT_FILE_PATH = "<REPL Input>"
1✔
20
REPL_NS = "basilisp.repl"
1✔
21
STDIN_INPUT_FILE_PATH = "<stdin>"
1✔
22
STDIN_FILE_NAME = "-"
1✔
23

24
BOOL_TRUE = frozenset({"true", "t", "1", "yes", "y"})
1✔
25
BOOL_FALSE = frozenset({"false", "f", "0", "no", "n"})
1✔
26

27
DEFAULT_COMPILER_OPTS = {k.name: v for k, v in compiler.compiler_opts().items()}
1✔
28

29

30
def eval_stream(stream, ctx: compiler.CompilerContext, ns: runtime.Namespace):
1✔
31
    """Evaluate the forms in stdin into a Python module AST node."""
32
    last = None
1✔
33
    for form in reader.read(stream, resolver=runtime.resolve_alias):
1✔
34
        assert not isinstance(form, reader.ReaderConditional)
1✔
35
        last = compiler.compile_and_exec_form(form, ctx, ns)
1✔
36
    return last
1✔
37

38

39
def eval_str(s: str, ctx: compiler.CompilerContext, ns: runtime.Namespace, eof: Any):
1✔
40
    """Evaluate the forms in a string into a Python module AST node."""
41
    last = eof
1✔
42
    for form in reader.read_str(s, resolver=runtime.resolve_alias, eof=eof):
1✔
43
        assert not isinstance(form, reader.ReaderConditional)
1✔
44
        last = compiler.compile_and_exec_form(form, ctx, ns)
1✔
45
    return last
1✔
46

47

48
def eval_file(filename: str, ctx: compiler.CompilerContext, ns: runtime.Namespace):
1✔
49
    """Evaluate a file with the given name into a Python module AST node."""
50
    return eval_str(f'(load-file "{filename}")', ctx, ns, eof=object())
1✔
51

52

53
def bootstrap_repl(ctx: compiler.CompilerContext, which_ns: str) -> types.ModuleType:
1✔
54
    """Bootstrap the REPL with a few useful vars and returned the bootstrapped
55
    module so it's functions can be used by the REPL command."""
56
    ns = runtime.Namespace.get_or_create(sym.symbol(which_ns))
1✔
57
    eval_str(f"(ns {sym.symbol(which_ns)} (:use basilisp.repl))", ctx, ns, object())
1✔
58
    return importlib.import_module(REPL_NS)
1✔
59

60

61
def _to_bool(v: Optional[str]) -> Optional[bool]:
1✔
62
    """Coerce a string argument to a boolean value, if possible."""
63
    if v is None:
1✔
64
        return v
×
65
    elif v.lower() in BOOL_TRUE:
1✔
66
        return True
1✔
67
    elif v.lower() in BOOL_FALSE:
1✔
68
        return False
1✔
69
    else:
70
        raise argparse.ArgumentTypeError("Unable to coerce flag value to boolean.")
1✔
71

72

73
def _set_envvar_action(
1✔
74
    var: str, parent: Type[argparse.Action] = argparse.Action
75
) -> Type[argparse.Action]:
76
    """Return an argparse.Action instance (deriving from `parent`) that sets the value
77
    as the default value of the environment variable `var`."""
78

79
    class EnvVarSetterAction(parent):  # type: ignore
1✔
80
        def __call__(  # pylint: disable=signature-differs
1✔
81
            self,
82
            parser: argparse.ArgumentParser,
83
            namespace: argparse.Namespace,
84
            values: Any,
85
            option_string: str,
86
        ):
87
            os.environ.setdefault(var, str(values))
1✔
88

89
    return EnvVarSetterAction
1✔
90

91

92
def _add_compiler_arg_group(parser: argparse.ArgumentParser) -> None:
1✔
93
    group = parser.add_argument_group(
1✔
94
        "compiler arguments",
95
        description=(
96
            "The compiler arguments below can be used to tweak warnings emitted by the "
97
            "compiler during compilation and in some cases, tweak emitted code. Note "
98
            "that Basilisp, like Python, aggressively caches compiled namespaces so "
99
            "you may need to disable namespace caching or modify your file to see the "
100
            "compiler argument changes take effect."
101
        ),
102
    )
103
    group.add_argument(
1✔
104
        "--generate-auto-inlines",
105
        action="store",
106
        nargs="?",
107
        const=os.getenv("BASILISP_GENERATE_AUTO_INLINES"),
108
        type=_to_bool,
109
        help=(
110
            "if true, the compiler will attempt to generate inline function defs "
111
            "for functions with a boolean `^:inline` meta key (env: "
112
            "BASILISP_GENERATE_AUTO_INLINES; default: "
113
            f"{DEFAULT_COMPILER_OPTS['generate-auto-inlines']})"
114
        ),
115
    )
116
    group.add_argument(
1✔
117
        "--inline-functions",
118
        action="store",
119
        nargs="?",
120
        const=os.getenv("BASILISP_INLINE_FUNCTIONS"),
121
        type=_to_bool,
122
        help=(
123
            "if true, the compiler will attempt to inline functions with an `^:inline` "
124
            "function definition at their invocation site (env: "
125
            "BASILISP_INLINE_FUNCTIONS; default: "
126
            f"{DEFAULT_COMPILER_OPTS['inline-functions']})"
127
        ),
128
    )
129
    group.add_argument(
1✔
130
        "--warn-on-shadowed-name",
131
        action="store",
132
        nargs="?",
133
        const=os.getenv("BASILISP_WARN_ON_SHADOWED_NAME"),
134
        type=_to_bool,
135
        help=(
136
            "if true, emit warnings if a local name is shadowed by another local "
137
            "name (env: BASILISP_WARN_ON_SHADOWED_NAME; default: "
138
            f"{DEFAULT_COMPILER_OPTS['warn-on-shadowed-name']})"
139
        ),
140
    )
141
    group.add_argument(
1✔
142
        "--warn-on-shadowed-var",
143
        action="store",
144
        nargs="?",
145
        const=os.getenv("BASILISP_WARN_ON_SHADOWED_VAR"),
146
        type=_to_bool,
147
        help=(
148
            "if true, emit warnings if a Var name is shadowed by a local name "
149
            "(env: BASILISP_WARN_ON_SHADOWED_VAR; default: "
150
            f"{DEFAULT_COMPILER_OPTS['warn-on-shadowed-var']})"
151
        ),
152
    )
153
    group.add_argument(
1✔
154
        "--warn-on-unused-names",
155
        action="store",
156
        nargs="?",
157
        const=os.getenv("BASILISP_WARN_ON_UNUSED_NAMES"),
158
        type=_to_bool,
159
        help=(
160
            "if true, emit warnings if a local name is bound and unused "
161
            "(env: BASILISP_WARN_ON_UNUSED_NAMES; default: "
162
            f"{DEFAULT_COMPILER_OPTS['warn-on-unused-names']})"
163
        ),
164
    )
165
    group.add_argument(
1✔
166
        "--warn-on-non-dynamic-set",
167
        action="store",
168
        nargs="?",
169
        const=os.getenv("BASILISP_WARN_ON_NON_DYNAMIC_SET"),
170
        type=_to_bool,
171
        help=(
172
            "if true, emit warnings if the compiler detects an attempt to set! "
173
            "a Var which is not marked as ^:dynamic (env: "
174
            "BASILISP_WARN_ON_NON_DYNAMIC_SET; default: "
175
            f"{DEFAULT_COMPILER_OPTS['warn-on-non-dynamic-set']})"
176
        ),
177
    )
178
    group.add_argument(
1✔
179
        "--use-var-indirection",
180
        action="store",
181
        nargs="?",
182
        const=os.getenv("BASILISP_USE_VAR_INDIRECTION"),
183
        type=_to_bool,
184
        help=(
185
            "if true, all Var accesses will be performed via Var indirection "
186
            "(env: BASILISP_USE_VAR_INDIRECTION; default: "
187
            f"{DEFAULT_COMPILER_OPTS['use-var-indirection']})"
188
        ),
189
    )
190
    group.add_argument(
1✔
191
        "--warn-on-var-indirection",
192
        action="store",
193
        nargs="?",
194
        const=os.getenv("BASILISP_WARN_ON_VAR_INDIRECTION"),
195
        type=_to_bool,
196
        help=(
197
            "if true, emit warnings if a Var reference cannot be direct linked "
198
            "(env: BASILISP_WARN_ON_VAR_INDIRECTION; default: "
199
            f"{DEFAULT_COMPILER_OPTS['warn-on-var-indirection']})"
200
        ),
201
    )
202

203

204
def _add_debug_arg_group(parser: argparse.ArgumentParser) -> None:
1✔
205
    group = parser.add_argument_group("debug options")
1✔
206
    group.add_argument(
1✔
207
        "--disable-ns-cache",
208
        action=_set_envvar_action(
209
            "BASILISP_DO_NOT_CACHE_NAMESPACES", parent=argparse._StoreAction
210
        ),
211
        nargs="?",
212
        const=True,
213
        type=_to_bool,
214
        help=(
215
            "if true, disable attempting to load cached namespaces "
216
            "(env: BASILISP_DO_NOT_CACHE_NAMESPACES; default: false)"
217
        ),
218
    )
219

220

221
Handler = Callable[[argparse.ArgumentParser, argparse.Namespace], None]
1✔
222

223

224
def _subcommand(
1✔
225
    subcommand: str,
226
    *,
227
    help: Optional[str] = None,  # pylint: disable=redefined-builtin
228
    description: Optional[str] = None,
229
    handler: Handler,
230
):
231
    def _wrap_add_subcommand(f: Callable[[argparse.ArgumentParser], None]):
1✔
232
        def _wrapped_subcommand(subparsers: "argparse._SubParsersAction"):
1✔
233
            parser = subparsers.add_parser(
1✔
234
                subcommand, help=help, description=description
235
            )
236
            parser.set_defaults(handler=handler)
1✔
237
            f(parser)
1✔
238

239
        return _wrapped_subcommand
1✔
240

241
    return _wrap_add_subcommand
1✔
242

243

244
def nrepl_server(
1✔
245
    _,
246
    args: argparse.Namespace,
247
):
248
    opts = compiler.compiler_opts()
1✔
249
    basilisp.init(opts)
1✔
250

251
    ctx = compiler.CompilerContext(filename=REPL_INPUT_FILE_PATH, opts=opts)
1✔
252
    eof = object()
1✔
253

254
    ns = runtime.Namespace.get_or_create(runtime.CORE_NS_SYM)
1✔
255
    host = runtime.lrepr(args.host)
1✔
256
    port = args.port
1✔
257
    port_filepath = runtime.lrepr(args.port_filepath)
1✔
258
    eval_str(
1✔
259
        (
260
            "(require '[basilisp.contrib.nrepl-server :as nr])"
261
            f"(nr/start-server! {{:host {host} :port {port} :nrepl-port-file {port_filepath}}})"
262
        ),
263
        ctx,
264
        ns,
265
        eof,
266
    )
267

268

269
@_subcommand(
1✔
270
    "nrepl-server",
271
    help="start the nREPL server",
272
    description="Start the nREPL server.",
273
    handler=nrepl_server,
274
)
275
def _add_nrepl_server_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
276
    parser.add_argument(
1✔
277
        "--host",
278
        default="127.0.0.1",
279
        help="the interface address to bind to, defaults to 127.0.0.1.",
280
    )
281
    parser.add_argument(
1✔
282
        "--port",
283
        default=0,
284
        type=int,
285
        help="the port to connect to, defaults to 0 (random available port).",
286
    )
287
    parser.add_argument(
1✔
288
        "--port-filepath",
289
        default=".nrepl-port",
290
        help='the file path where the server port number is output to, defaults to ".nrepl-port".',
291
    )
292

293

294
def repl(
1✔
295
    _,
296
    args: argparse.Namespace,
297
):
298
    opts = compiler.compiler_opts(
1✔
299
        warn_on_shadowed_name=args.warn_on_shadowed_name,
300
        warn_on_shadowed_var=args.warn_on_shadowed_var,
301
        warn_on_unused_names=args.warn_on_unused_names,
302
        use_var_indirection=args.use_var_indirection,
303
        warn_on_var_indirection=args.warn_on_var_indirection,
304
    )
305
    basilisp.init(opts)
1✔
306
    ctx = compiler.CompilerContext(filename=REPL_INPUT_FILE_PATH, opts=opts)
1✔
307
    prompter = get_prompter()
1✔
308
    eof = object()
1✔
309

310
    # Bind user-settable dynamic Vars to their existing value to allow users to
311
    # conveniently (set! *var* val) at the REPL without needing `binding`.
312
    with runtime.bindings(
1✔
313
        {
314
            var: var.value
315
            for var in map(
316
                lambda name: runtime.Var.find_safe(
317
                    sym.symbol(name, ns=runtime.CORE_NS)
318
                ),
319
                [
320
                    "*e",
321
                    "*1",
322
                    "*2",
323
                    "*3",
324
                    "*assert*",
325
                    "*data-readers*",
326
                    "*resolver*",
327
                    runtime.PRINT_DUP_VAR_NAME,
328
                    runtime.PRINT_LEVEL_VAR_NAME,
329
                    runtime.PRINT_READABLY_VAR_NAME,
330
                    runtime.PRINT_LEVEL_VAR_NAME,
331
                    runtime.PRINT_META_VAR_NAME,
332
                ],
333
            )
334
        }
335
    ):
336
        repl_module = bootstrap_repl(ctx, args.default_ns)
1✔
337
        ns_var = runtime.set_current_ns(args.default_ns)
1✔
338

339
        while True:
1✔
340
            ns: runtime.Namespace = ns_var.value
1✔
341
            try:
1✔
342
                lsrc = prompter.prompt(f"{ns.name}=> ")
1✔
343
            except EOFError:
1✔
344
                break
1✔
345
            except KeyboardInterrupt:  # pragma: no cover
346
                print("")
347
                continue
348

349
            if len(lsrc) == 0:
1✔
350
                continue
1✔
351

352
            try:
1✔
353
                result = eval_str(lsrc, ctx, ns, eof)
1✔
354
                if result is eof:  # pragma: no cover
355
                    continue
356
                prompter.print(runtime.lrepr(result))
1✔
357
                repl_module.mark_repl_result(result)
1✔
358
            except reader.SyntaxError as e:
1✔
359
                traceback.print_exception(reader.SyntaxError, e, e.__traceback__)
1✔
360
                repl_module.mark_exception(e)
1✔
361
                continue
1✔
362
            except compiler.CompilerException as e:
1✔
363
                traceback.print_exception(
1✔
364
                    compiler.CompilerException, e, e.__traceback__
365
                )
366
                repl_module.mark_exception(e)
1✔
367
                continue
1✔
368
            except Exception as e:  # pylint: disable=broad-exception-caught
1✔
369
                traceback.print_exception(Exception, e, e.__traceback__)
1✔
370
                repl_module.mark_exception(e)
1✔
371
                continue
1✔
372

373

374
@_subcommand(
1✔
375
    "repl",
376
    help="start the Basilisp REPL",
377
    description="Start a Basilisp REPL.",
378
    handler=repl,
379
)
380
def _add_repl_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
381
    parser.add_argument(
1✔
382
        "--default-ns",
383
        default=runtime.REPL_DEFAULT_NS,
384
        help="default namespace to use for the REPL",
385
    )
386
    _add_compiler_arg_group(parser)
1✔
387
    _add_debug_arg_group(parser)
1✔
388

389

390
def run(
1✔
391
    _,
392
    args: argparse.Namespace,
393
):
394
    opts = compiler.compiler_opts(
1✔
395
        warn_on_shadowed_name=args.warn_on_shadowed_name,
396
        warn_on_shadowed_var=args.warn_on_shadowed_var,
397
        warn_on_unused_names=args.warn_on_unused_names,
398
        use_var_indirection=args.use_var_indirection,
399
        warn_on_var_indirection=args.warn_on_var_indirection,
400
    )
401
    basilisp.init(opts)
1✔
402
    ctx = compiler.CompilerContext(
1✔
403
        filename=CLI_INPUT_FILE_PATH
404
        if args.code
405
        else (
406
            STDIN_INPUT_FILE_PATH
407
            if args.file_or_code == STDIN_FILE_NAME
408
            else args.file_or_code
409
        ),
410
        opts=opts,
411
    )
412
    eof = object()
1✔
413

414
    core_ns = runtime.Namespace.get(runtime.CORE_NS_SYM)
1✔
415
    assert core_ns is not None
1✔
416

417
    with runtime.ns_bindings(args.in_ns) as ns:
1✔
418
        ns.refer_all(core_ns)
1✔
419

420
        if args.code:
1✔
421
            eval_str(args.file_or_code, ctx, ns, eof)
1✔
422
        elif args.file_or_code == STDIN_FILE_NAME:
1✔
423
            eval_stream(io.TextIOWrapper(sys.stdin.buffer, encoding="utf-8"), ctx, ns)
1✔
424
        else:
425
            eval_file(args.file_or_code, ctx, ns)
1✔
426

427

428
@_subcommand(
1✔
429
    "run",
430
    help="run a Basilisp script or code",
431
    description="Run a Basilisp script or a line of code, if it is provided.",
432
    handler=run,
433
)
434
def _add_run_subcommand(parser: argparse.ArgumentParser):
1✔
435
    parser.add_argument(
1✔
436
        "file_or_code",
437
        help="file path to a Basilisp file or, if -c is provided, a string of Basilisp code",
438
    )
439
    parser.add_argument(
1✔
440
        "-c",
441
        "--code",
442
        action="store_true",
443
        help="if provided, treat argument as a string of code",
444
    )
445
    parser.add_argument(
1✔
446
        "--in-ns", default=runtime.REPL_DEFAULT_NS, help="namespace to use for the code"
447
    )
448
    _add_compiler_arg_group(parser)
1✔
449
    _add_debug_arg_group(parser)
1✔
450

451

452
def test(parser: argparse.ArgumentParser, args: argparse.Namespace):  # pragma: no cover
453
    try:
454
        import pytest
455
    except (ImportError, ModuleNotFoundError):
456
        parser.error(
457
            "Cannot run tests without dependency PyTest. Please install PyTest and try again.",
458
        )
459
    else:
460
        pytest.main(args=list(args.args))
461

462

463
@_subcommand(
1✔
464
    "test",
465
    help="run tests in a Basilisp project",
466
    description="Run tests in a Basilisp project.",
467
    handler=test,
468
)
469
def _add_test_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
470
    parser.add_argument("args", nargs=-1)
1✔
471

472

473
def version(_, __):
1✔
474
    v = importlib.metadata.version("basilisp")
1✔
475
    print(f"Basilisp {v}")
1✔
476

477

478
@_subcommand("version", help="print the version of Basilisp", handler=version)
1✔
479
def _add_version_subcommand(_: argparse.ArgumentParser) -> None:
1✔
480
    pass
1✔
481

482

483
def run_script():
1✔
484
    """Entrypoint to run the Basilisp script named by `sys.argv[1]` as by the
485
    `basilisp run` subcommand.
486

487
    This is provided as a shim for platforms where shebang lines cannot contain more
488
    than one argument and thus `#!/usr/bin/env basilisp run` would be non-functional.
489

490
    The current process is replaced as by `os.execlp`."""
491
    # os.exec* functions do not perform shell expansion, so we must do so manually.
492
    script_path = Path(sys.argv[1]).resolve()
×
493
    os.execlp("basilisp", "basilisp", "run", script_path)
×
494

495

496
def invoke_cli(args: Optional[Sequence[str]] = None) -> None:
1✔
497
    """Entrypoint to run the Basilisp CLI."""
498
    parser = argparse.ArgumentParser(
1✔
499
        description="Basilisp is a Lisp dialect inspired by Clojure targeting Python 3."
500
    )
501

502
    subparsers = parser.add_subparsers(help="sub-commands")
1✔
503
    _add_nrepl_server_subcommand(subparsers)
1✔
504
    _add_repl_subcommand(subparsers)
1✔
505
    _add_run_subcommand(subparsers)
1✔
506
    _add_test_subcommand(subparsers)
1✔
507
    _add_version_subcommand(subparsers)
1✔
508

509
    parsed_args = parser.parse_args(args=args)
1✔
510
    if hasattr(parsed_args, "handler"):
1✔
511
        parsed_args.handler(parser, parsed_args)
1✔
512
    else:
513
        parser.print_help()
×
514

515

516
if __name__ == "__main__":
517
    invoke_cli()
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc