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

basilisp-lang / basilisp / 10943311926

19 Sep 2024 02:41PM CUT coverage: 98.89%. Remained the same
10943311926

Pull #1062

github

web-flow
Merge 1e881487b into 86c59ead1
Pull Request #1062: Prepare for release v0.2.3

1903 of 1910 branches covered (99.63%)

Branch coverage included in aggregate %.

8695 of 8807 relevant lines covered (98.73%)

0.99 hits per line

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

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

12
from basilisp import main as basilisp
1✔
13
from basilisp.lang import compiler as compiler
1✔
14
from basilisp.lang import keyword as kw
1✔
15
from basilisp.lang import list as llist
1✔
16
from basilisp.lang import map as lmap
1✔
17
from basilisp.lang import reader as reader
1✔
18
from basilisp.lang import runtime as runtime
1✔
19
from basilisp.lang import symbol as sym
1✔
20
from basilisp.lang import vector as vec
1✔
21
from basilisp.lang.exception import print_exception
1✔
22
from basilisp.lang.typing import CompilerOpts
1✔
23
from basilisp.lang.util import munge
1✔
24
from basilisp.prompt import get_prompter
1✔
25

26
CLI_INPUT_FILE_PATH = "<CLI Input>"
1✔
27
REPL_INPUT_FILE_PATH = "<REPL Input>"
1✔
28
REPL_NS = "basilisp.repl"
1✔
29
NREPL_SERVER_NS = "basilisp.contrib.nrepl-server"
1✔
30
STDIN_INPUT_FILE_PATH = "<stdin>"
1✔
31
STDIN_FILE_NAME = "-"
1✔
32

33
BOOL_TRUE = frozenset({"true", "t", "1", "yes", "y"})
1✔
34
BOOL_FALSE = frozenset({"false", "f", "0", "no", "n"})
1✔
35

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

38

39
def eval_stream(stream, ctx: compiler.CompilerContext, ns: runtime.Namespace):
1✔
40
    """Evaluate the forms in stdin into a Python module AST node."""
41
    last = None
1✔
42
    for form in reader.read(stream, resolver=runtime.resolve_alias):
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_str(s: str, ctx: compiler.CompilerContext, ns: runtime.Namespace, eof: Any):
1✔
49
    """Evaluate the forms in a string into a Python module AST node."""
50
    last = eof
1✔
51
    for form in reader.read_str(s, resolver=runtime.resolve_alias, eof=eof):
1✔
52
        assert not isinstance(form, reader.ReaderConditional)
1✔
53
        last = compiler.compile_and_exec_form(form, ctx, ns)
1✔
54
    return last
1✔
55

56

57
def eval_file(filename: str, ctx: compiler.CompilerContext, ns: runtime.Namespace):
1✔
58
    """Evaluate a file with the given name into a Python module AST node."""
59
    if (path := Path(filename)).exists():
1✔
60
        return compiler.load_file(path, ctx, ns)
1✔
61
    else:
62
        raise FileNotFoundError(f"Error: The file {filename} does not exist.")
1✔
63

64

65
def bootstrap_repl(ctx: compiler.CompilerContext, which_ns: str) -> types.ModuleType:
1✔
66
    """Bootstrap the REPL with a few useful vars and returned the bootstrapped
67
    module so it's functions can be used by the REPL command."""
68
    which_ns_sym = sym.symbol(which_ns)
1✔
69
    ns = runtime.Namespace.get_or_create(which_ns_sym)
1✔
70
    compiler.compile_and_exec_form(
1✔
71
        llist.l(
72
            sym.symbol("ns", ns=runtime.CORE_NS),
73
            which_ns_sym,
74
            llist.l(kw.keyword("use"), sym.symbol(REPL_NS)),
75
        ),
76
        ctx,
77
        ns,
78
    )
79
    return importlib.import_module(REPL_NS)
1✔
80

81

82
def init_path(args: argparse.Namespace, unsafe_path: str = "") -> None:
1✔
83
    """Prepend any import group arguments to `sys.path`, including `unsafe_path` (which
84
    defaults to the empty string) if --include-unsafe-path is specified."""
85

86
    def prepend_once(path: str) -> None:
1✔
87
        if path in sys.path:
1✔
88
            return
×
89
        sys.path.insert(0, path)
1✔
90

91
    for pth in args.include_path or []:
1✔
92
        p = pathlib.Path(pth).resolve()
1✔
93
        prepend_once(str(p))
1✔
94

95
    if args.include_unsafe_path:
1✔
96
        prepend_once(unsafe_path)
1✔
97

98

99
def _to_bool(v: Optional[str]) -> Optional[bool]:
1✔
100
    """Coerce a string argument to a boolean value, if possible."""
101
    if v is None:
1✔
102
        return v
×
103
    elif v.lower() in BOOL_TRUE:
1✔
104
        return True
1✔
105
    elif v.lower() in BOOL_FALSE:
1✔
106
        return False
1✔
107
    else:
108
        raise argparse.ArgumentTypeError("Unable to coerce flag value to boolean.")
1✔
109

110

111
def _set_envvar_action(
1✔
112
    var: str, parent: Type[argparse.Action] = argparse.Action
113
) -> Type[argparse.Action]:
114
    """Return an argparse.Action instance (deriving from `parent`) that sets the value
115
    as the default value of the environment variable `var`."""
116

117
    class EnvVarSetterAction(parent):  # type: ignore
1✔
118
        def __call__(  # pylint: disable=signature-differs
1✔
119
            self,
120
            parser: argparse.ArgumentParser,
121
            namespace: argparse.Namespace,
122
            values: Any,
123
            option_string: str,
124
        ):
125
            os.environ.setdefault(var, str(values))
1✔
126

127
    return EnvVarSetterAction
1✔
128

129

130
def _add_compiler_arg_group(parser: argparse.ArgumentParser) -> None:
1✔
131
    group = parser.add_argument_group(
1✔
132
        "compiler arguments",
133
        description=(
134
            "The compiler arguments below can be used to tweak warnings emitted by the "
135
            "compiler during compilation and in some cases, tweak emitted code. Note "
136
            "that Basilisp, like Python, aggressively caches compiled namespaces so "
137
            "you may need to disable namespace caching or modify your file to see the "
138
            "compiler argument changes take effect."
139
        ),
140
    )
141
    group.add_argument(
1✔
142
        "--generate-auto-inlines",
143
        action="store",
144
        nargs="?",
145
        const=os.getenv("BASILISP_GENERATE_AUTO_INLINES"),
146
        type=_to_bool,
147
        help=(
148
            "if true, the compiler will attempt to generate inline function defs "
149
            "for functions with a boolean `^:inline` meta key (env: "
150
            "BASILISP_GENERATE_AUTO_INLINES; default: "
151
            f"{DEFAULT_COMPILER_OPTS['generate-auto-inlines']})"
152
        ),
153
    )
154
    group.add_argument(
1✔
155
        "--inline-functions",
156
        action="store",
157
        nargs="?",
158
        const=os.getenv("BASILISP_INLINE_FUNCTIONS"),
159
        type=_to_bool,
160
        help=(
161
            "if true, the compiler will attempt to inline functions with an `^:inline` "
162
            "function definition at their invocation site (env: "
163
            "BASILISP_INLINE_FUNCTIONS; default: "
164
            f"{DEFAULT_COMPILER_OPTS['inline-functions']})"
165
        ),
166
    )
167
    group.add_argument(
1✔
168
        "--warn-on-arity-mismatch",
169
        action="store",
170
        nargs="?",
171
        const=os.getenv("BASILISP_WARN_ON_ARITY_MISMATCH"),
172
        type=_to_bool,
173
        help=(
174
            "if true, emit warnings if a Basilisp function invocation is detected with "
175
            "an unsupported number of arguments "
176
            "(env: BASILISP_WARN_ON_ARITY_MISMATCH; default: "
177
            f"{DEFAULT_COMPILER_OPTS['warn-on-arity-mismatch']})"
178
        ),
179
    )
180
    group.add_argument(
1✔
181
        "--warn-on-shadowed-name",
182
        action="store",
183
        nargs="?",
184
        const=os.getenv("BASILISP_WARN_ON_SHADOWED_NAME"),
185
        type=_to_bool,
186
        help=(
187
            "if true, emit warnings if a local name is shadowed by another local "
188
            "name (env: BASILISP_WARN_ON_SHADOWED_NAME; default: "
189
            f"{DEFAULT_COMPILER_OPTS['warn-on-shadowed-name']})"
190
        ),
191
    )
192
    group.add_argument(
1✔
193
        "--warn-on-shadowed-var",
194
        action="store",
195
        nargs="?",
196
        const=os.getenv("BASILISP_WARN_ON_SHADOWED_VAR"),
197
        type=_to_bool,
198
        help=(
199
            "if true, emit warnings if a Var name is shadowed by a local name "
200
            "(env: BASILISP_WARN_ON_SHADOWED_VAR; default: "
201
            f"{DEFAULT_COMPILER_OPTS['warn-on-shadowed-var']})"
202
        ),
203
    )
204
    group.add_argument(
1✔
205
        "--warn-on-unused-names",
206
        action="store",
207
        nargs="?",
208
        const=os.getenv("BASILISP_WARN_ON_UNUSED_NAMES"),
209
        type=_to_bool,
210
        help=(
211
            "if true, emit warnings if a local name is bound and unused "
212
            "(env: BASILISP_WARN_ON_UNUSED_NAMES; default: "
213
            f"{DEFAULT_COMPILER_OPTS['warn-on-unused-names']})"
214
        ),
215
    )
216
    group.add_argument(
1✔
217
        "--warn-on-non-dynamic-set",
218
        action="store",
219
        nargs="?",
220
        const=os.getenv("BASILISP_WARN_ON_NON_DYNAMIC_SET"),
221
        type=_to_bool,
222
        help=(
223
            "if true, emit warnings if the compiler detects an attempt to set! "
224
            "a Var which is not marked as ^:dynamic (env: "
225
            "BASILISP_WARN_ON_NON_DYNAMIC_SET; default: "
226
            f"{DEFAULT_COMPILER_OPTS['warn-on-non-dynamic-set']})"
227
        ),
228
    )
229
    group.add_argument(
1✔
230
        "--use-var-indirection",
231
        action="store",
232
        nargs="?",
233
        const=os.getenv("BASILISP_USE_VAR_INDIRECTION"),
234
        type=_to_bool,
235
        help=(
236
            "if true, all Var accesses will be performed via Var indirection "
237
            "(env: BASILISP_USE_VAR_INDIRECTION; default: "
238
            f"{DEFAULT_COMPILER_OPTS['use-var-indirection']})"
239
        ),
240
    )
241
    group.add_argument(
1✔
242
        "--warn-on-var-indirection",
243
        action="store",
244
        nargs="?",
245
        const=os.getenv("BASILISP_WARN_ON_VAR_INDIRECTION"),
246
        type=_to_bool,
247
        help=(
248
            "if true, emit warnings if a Var reference cannot be direct linked "
249
            "(env: BASILISP_WARN_ON_VAR_INDIRECTION; default: "
250
            f"{DEFAULT_COMPILER_OPTS['warn-on-var-indirection']})"
251
        ),
252
    )
253

254

255
def _compiler_opts(args: argparse.Namespace) -> CompilerOpts:
1✔
256
    return compiler.compiler_opts(
1✔
257
        generate_auto_inlines=args.generate_auto_inlines,
258
        inline_functions=args.inline_functions,
259
        warn_on_arity_mismatch=args.warn_on_arity_mismatch,
260
        warn_on_shadowed_name=args.warn_on_shadowed_name,
261
        warn_on_shadowed_var=args.warn_on_shadowed_var,
262
        warn_on_non_dynamic_set=args.warn_on_non_dynamic_set,
263
        warn_on_unused_names=args.warn_on_unused_names,
264
        use_var_indirection=args.use_var_indirection,
265
        warn_on_var_indirection=args.warn_on_var_indirection,
266
    )
267

268

269
def _add_debug_arg_group(parser: argparse.ArgumentParser) -> None:
1✔
270
    group = parser.add_argument_group("debug options")
1✔
271
    group.add_argument(
1✔
272
        "--disable-ns-cache",
273
        action=_set_envvar_action(
274
            "BASILISP_DO_NOT_CACHE_NAMESPACES", parent=argparse._StoreAction
275
        ),
276
        nargs="?",
277
        const=True,
278
        type=_to_bool,
279
        help=(
280
            "if true, disable attempting to load cached namespaces "
281
            "(env: BASILISP_DO_NOT_CACHE_NAMESPACES; default: false)"
282
        ),
283
    )
284
    group.add_argument(
1✔
285
        "--enable-logger",
286
        action=_set_envvar_action(
287
            "BASILISP_USE_DEV_LOGGER", parent=argparse._StoreAction
288
        ),
289
        nargs="?",
290
        const=True,
291
        type=_to_bool,
292
        help=(
293
            "if true, enable the Basilisp root logger "
294
            "(env: BASILISP_USE_DEV_LOGGER; default: false)"
295
        ),
296
    )
297
    group.add_argument(
1✔
298
        "-l",
299
        "--log-level",
300
        action=_set_envvar_action(
301
            "BASILISP_LOGGING_LEVEL", parent=argparse._StoreAction
302
        ),
303
        type=lambda s: s.upper(),
304
        default="WARNING",
305
        help=(
306
            "the logging level for logs emitted by the Basilisp compiler and runtime "
307
            "(env: BASILISP_LOGGING_LEVEL; default: WARNING)"
308
        ),
309
    )
310
    group.add_argument(
1✔
311
        "--emit-generated-python",
312
        action=_set_envvar_action(
313
            "BASILISP_EMIT_GENERATED_PYTHON", parent=argparse._StoreAction
314
        ),
315
        nargs="?",
316
        const=True,
317
        type=_to_bool,
318
        help=(
319
            "if true, store generated Python code in `*generated-python*` dynamic "
320
            "Vars within each namespace (env: BASILISP_EMIT_GENERATED_PYTHON; "
321
            "default: true)"
322
        ),
323
    )
324

325

326
def _add_import_arg_group(parser: argparse.ArgumentParser) -> None:
1✔
327
    group = parser.add_argument_group(
1✔
328
        "path options",
329
        description=(
330
            "The path options below can be used to control how Basilisp (and Python) "
331
            "find your code."
332
        ),
333
    )
334
    group.add_argument(
1✔
335
        "--include-unsafe-path",
336
        action="store",
337
        nargs="?",
338
        const=True,
339
        default=os.getenv("BASILISP_INCLUDE_UNSAFE_PATH", "true"),
340
        type=_to_bool,
341
        help=(
342
            "if true, automatically prepend a potentially unsafe path to `sys.path`; "
343
            "setting `--include-unsafe-path=false` is the Basilisp equivalent to "
344
            "setting PYTHONSAFEPATH to a non-empty string for CPython's REPL "
345
            "(env: BASILISP_INCLUDE_UNSAFE_PATH; default: true)"
346
        ),
347
    )
348
    group.add_argument(
1✔
349
        "-p",
350
        "--include-path",
351
        action="append",
352
        help=(
353
            "path to prepend to `sys.path`; may be specified more than once to "
354
            "include multiple paths (env: PYTHONPATH)"
355
        ),
356
    )
357

358

359
def _add_runtime_arg_group(parser: argparse.ArgumentParser) -> None:
1✔
360
    group = parser.add_argument_group(
1✔
361
        "runtime arguments",
362
        description=(
363
            "The runtime arguments below affect reader and execution time features."
364
        ),
365
    )
366
    group.add_argument(
1✔
367
        "--data-readers-entry-points",
368
        action=_set_envvar_action(
369
            "BASILISP_USE_DATA_READERS_ENTRY_POINT", parent=argparse._StoreAction
370
        ),
371
        nargs="?",
372
        const=_to_bool(os.getenv("BASILISP_USE_DATA_READERS_ENTRY_POINT", "true")),
373
        type=_to_bool,
374
        help=(
375
            "if true, Load data readers from importlib entry points in the "
376
            '"basilisp_data_readers" group (env: '
377
            "BASILISP_USE_DATA_READERS_ENTRY_POINT; default: true)"
378
        ),
379
    )
380

381

382
Handler = Callable[[argparse.ArgumentParser, argparse.Namespace], None]
1✔
383

384

385
def _subcommand(
1✔
386
    subcommand: str,
387
    *,
388
    help: Optional[str] = None,  # pylint: disable=redefined-builtin
389
    description: Optional[str] = None,
390
    handler: Handler,
391
) -> Callable[
392
    [Callable[[argparse.ArgumentParser], None]],
393
    Callable[["argparse._SubParsersAction"], None],
394
]:
395
    def _wrap_add_subcommand(
1✔
396
        f: Callable[[argparse.ArgumentParser], None]
397
    ) -> Callable[["argparse._SubParsersAction"], None]:
398
        def _wrapped_subcommand(subparsers: "argparse._SubParsersAction"):
1✔
399
            parser = subparsers.add_parser(
1✔
400
                subcommand, help=help, description=description
401
            )
402
            parser.set_defaults(handler=handler)
1✔
403
            f(parser)
1✔
404

405
        return _wrapped_subcommand
1✔
406

407
    return _wrap_add_subcommand
1✔
408

409

410
def bootstrap_basilisp_installation(_, args: argparse.Namespace) -> None:
1✔
411
    if args.quiet:
1✔
412
        print_ = lambda v: v
1✔
413
    else:
414
        print_ = print
1✔
415

416
    if args.uninstall:
1✔
417
        if not (
1✔
418
            removed := basilisp.unbootstrap_python(site_packages=args.site_packages)
419
        ):
420
            print_("No Basilisp bootstrap files were found.")
1✔
421
        else:
422
            for file in removed:
1✔
423
                print_(f"Removed '{file}'")
1✔
424
    else:
425
        basilisp.bootstrap_python(site_packages=args.site_packages)
1✔
426
        print_(
1✔
427
            "Your Python installation has been bootstrapped! You can undo this at any "
428
            "time with with `basilisp bootstrap --uninstall`."
429
        )
430

431

432
@_subcommand(
1✔
433
    "bootstrap",
434
    help="bootstrap the Python installation to allow importing Basilisp namespaces",
435
    description=textwrap.dedent(
436
        """Bootstrap the Python installation to allow importing Basilisp namespaces"
437
        without requiring an additional bootstrapping step.
438

439
        Python installations are bootstrapped by installing a `basilispbootstrap.pth`
440
        file in your `site-packages` directory. Python installations execute `*.pth`
441
        files found at startup.
442

443
        Bootstrapping your Python installation in this way can help avoid needing to
444
        perform manual bootstrapping from Python code within your application.
445

446
        On the first startup, Basilisp will compile `basilisp.core` to byte code
447
        which could take up to 30 seconds in some cases depending on your system and
448
        which version of Python you are using. Subsequent startups should be
449
        considerably faster so long as you allow Basilisp to cache bytecode for
450
        namespaces."""
451
    ),
452
    handler=bootstrap_basilisp_installation,
453
)
454
def _add_bootstrap_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
455
    parser.add_argument(
1✔
456
        "--uninstall",
457
        action="store_true",
458
        help="if true, remove any `.pth` files installed by Basilisp in all site-packages directories",
459
    )
460
    parser.add_argument(
1✔
461
        "-q",
462
        "--quiet",
463
        action="store_true",
464
        help="if true, do not print out any",
465
    )
466
    # Allow specifying the "site-packages" directories via CLI argument for testing.
467
    # Not intended to be used by end users.
468
    parser.add_argument(
1✔
469
        "--site-packages",
470
        action="append",
471
        help=argparse.SUPPRESS,
472
    )
473

474

475
def nrepl_server(
1✔
476
    _,
477
    args: argparse.Namespace,
478
) -> None:
479
    basilisp.init(_compiler_opts(args))
1✔
480
    init_path(args)
1✔
481
    nrepl_server_mod = importlib.import_module(munge(NREPL_SERVER_NS))
1✔
482
    nrepl_server_mod.start_server__BANG__(
1✔
483
        lmap.map(
484
            {
485
                kw.keyword("host"): args.host,
486
                kw.keyword("port"): args.port,
487
                kw.keyword("nrepl-port-file"): args.port_filepath,
488
            }
489
        )
490
    )
491

492

493
@_subcommand(
1✔
494
    "nrepl-server",
495
    help="start the nREPL server",
496
    description="Start the nREPL server.",
497
    handler=nrepl_server,
498
)
499
def _add_nrepl_server_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
500
    parser.add_argument(
1✔
501
        "--host",
502
        default="127.0.0.1",
503
        help="the interface address to bind to, defaults to 127.0.0.1.",
504
    )
505
    parser.add_argument(
1✔
506
        "--port",
507
        default=0,
508
        type=int,
509
        help="the port to connect to, defaults to 0 (random available port).",
510
    )
511
    parser.add_argument(
1✔
512
        "--port-filepath",
513
        default=".nrepl-port",
514
        help='the file path where the server port number is output to, defaults to ".nrepl-port".',
515
    )
516
    _add_compiler_arg_group(parser)
1✔
517
    _add_import_arg_group(parser)
1✔
518
    _add_runtime_arg_group(parser)
1✔
519
    _add_debug_arg_group(parser)
1✔
520

521

522
def repl(
1✔
523
    _,
524
    args: argparse.Namespace,
525
) -> None:
526
    opts = _compiler_opts(args)
1✔
527
    basilisp.init(opts)
1✔
528
    init_path(args)
1✔
529
    ctx = compiler.CompilerContext(filename=REPL_INPUT_FILE_PATH, opts=opts)
1✔
530
    prompter = get_prompter()
1✔
531
    eof = object()
1✔
532

533
    # Bind user-settable dynamic Vars to their existing value to allow users to
534
    # conveniently (set! *var* val) at the REPL without needing `binding`.
535
    with runtime.bindings(
1✔
536
        {
537
            var: var.value
538
            for var in map(
539
                lambda name: runtime.Var.find_safe(
540
                    sym.symbol(name, ns=runtime.CORE_NS)
541
                ),
542
                [
543
                    "*e",
544
                    "*1",
545
                    "*2",
546
                    "*3",
547
                    "*assert*",
548
                    "*data-readers*",
549
                    "*resolver*",
550
                    runtime.PRINT_DUP_VAR_NAME,
551
                    runtime.PRINT_LEVEL_VAR_NAME,
552
                    runtime.PRINT_READABLY_VAR_NAME,
553
                    runtime.PRINT_LEVEL_VAR_NAME,
554
                    runtime.PRINT_META_VAR_NAME,
555
                    runtime.PRINT_NAMESPACE_MAPS_VAR_NAME,
556
                ],
557
            )
558
        }
559
    ):
560
        repl_module = bootstrap_repl(ctx, args.default_ns)
1✔
561
        ns_var = runtime.set_current_ns(args.default_ns)
1✔
562

563
        while True:
1✔
564
            ns: runtime.Namespace = ns_var.value
1✔
565
            try:
1✔
566
                lsrc = prompter.prompt(f"{ns.name}=> ")
1✔
567
            except EOFError:
1✔
568
                break
1✔
569
            except KeyboardInterrupt:  # pragma: no cover
570
                print("")
571
                continue
572

573
            if len(lsrc) == 0:
1✔
574
                continue
1✔
575

576
            try:
1✔
577
                result = eval_str(lsrc, ctx, ns, eof)
1✔
578
                if result is eof:  # pragma: no cover
579
                    continue
580
                prompter.print(runtime.lrepr(result))
1✔
581
                repl_module.mark_repl_result(result)
1✔
582
            except reader.SyntaxError as e:
1✔
583
                print_exception(e, reader.SyntaxError, e.__traceback__)
1✔
584
                repl_module.mark_exception(e)
1✔
585
                continue
1✔
586
            except compiler.CompilerException as e:
1✔
587
                print_exception(e, compiler.CompilerException, e.__traceback__)
1✔
588
                repl_module.mark_exception(e)
1✔
589
                continue
1✔
590
            except Exception as e:  # pylint: disable=broad-exception-caught
1✔
591
                print_exception(e, Exception, e.__traceback__)
1✔
592
                repl_module.mark_exception(e)
1✔
593
                continue
1✔
594

595

596
@_subcommand(
1✔
597
    "repl",
598
    help="start the Basilisp REPL",
599
    description="Start a Basilisp REPL.",
600
    handler=repl,
601
)
602
def _add_repl_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
603
    parser.add_argument(
1✔
604
        "--default-ns",
605
        default=runtime.REPL_DEFAULT_NS,
606
        help="default namespace to use for the REPL",
607
    )
608
    _add_compiler_arg_group(parser)
1✔
609
    _add_import_arg_group(parser)
1✔
610
    _add_runtime_arg_group(parser)
1✔
611
    _add_debug_arg_group(parser)
1✔
612

613

614
def run(
1✔
615
    parser: argparse.ArgumentParser,
616
    args: argparse.Namespace,
617
) -> None:
618
    target = args.file_or_ns_or_code
1✔
619
    if args.load_namespace:
1✔
620
        if args.in_ns is not None:
1✔
621
            parser.error(
1✔
622
                "argument --in-ns: not allowed with argument -n/--load-namespace"
623
            )
624
        in_ns = runtime.REPL_DEFAULT_NS
1✔
625
    else:
626
        in_ns = target if args.in_ns is not None else runtime.REPL_DEFAULT_NS
1✔
627

628
    opts = _compiler_opts(args)
1✔
629
    basilisp.init(opts)
1✔
630
    ctx = compiler.CompilerContext(
1✔
631
        filename=(
632
            CLI_INPUT_FILE_PATH
633
            if args.code
634
            else (STDIN_INPUT_FILE_PATH if target == STDIN_FILE_NAME else target)
635
        ),
636
        opts=opts,
637
    )
638
    eof = object()
1✔
639

640
    core_ns = runtime.Namespace.get(runtime.CORE_NS_SYM)
1✔
641
    assert core_ns is not None
1✔
642

643
    with runtime.ns_bindings(in_ns) as ns:
1✔
644
        ns.refer_all(core_ns)
1✔
645

646
        if args.args:
1✔
647
            cli_args_var = core_ns.find(sym.symbol(runtime.COMMAND_LINE_ARGS_VAR_NAME))
1✔
648
            assert cli_args_var is not None
1✔
649
            cli_args_var.bind_root(vec.vector(args.args))
1✔
650

651
        if args.code:
1✔
652
            init_path(args)
1✔
653
            eval_str(target, ctx, ns, eof)
1✔
654
        elif args.load_namespace:
1✔
655
            # Set the requested namespace as the *main-ns*
656
            main_ns_var = core_ns.find(sym.symbol(runtime.MAIN_NS_VAR_NAME))
1✔
657
            assert main_ns_var is not None
1✔
658
            main_ns_var.bind_root(sym.symbol(target))
1✔
659

660
            init_path(args)
1✔
661
            importlib.import_module(munge(target))
1✔
662
        elif target == STDIN_FILE_NAME:
1✔
663
            init_path(args)
1✔
664
            eval_stream(io.TextIOWrapper(sys.stdin.buffer, encoding="utf-8"), ctx, ns)
1✔
665
        else:
666
            init_path(args, unsafe_path=str(pathlib.Path(target).resolve().parent))
1✔
667
            eval_file(target, ctx, ns)
1✔
668

669

670
@_subcommand(
1✔
671
    "run",
672
    help="run a Basilisp script or code or namespace",
673
    description=textwrap.dedent(
674
        """Run a Basilisp script or a line of code or load a Basilisp namespace.
675

676
        If `-c` is provided, execute the line of code as given. If `-n` is given,
677
        interpret `file_or_ns_or_code` as a fully qualified Basilisp namespace
678
        relative to `sys.path`. Otherwise, execute the file as a script relative to
679
        the current working directory.
680

681
        `*main-ns*` will be set to the value provided for `-n`. In all other cases,
682
        it will be `nil`."""
683
    ),
684
    handler=run,
685
)
686
def _add_run_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
687
    parser.add_argument(
1✔
688
        "file_or_ns_or_code",
689
        help=(
690
            "file path to a Basilisp file, a string of Basilisp code, or a fully "
691
            "qualified Basilisp namespace name"
692
        ),
693
    )
694

695
    grp = parser.add_mutually_exclusive_group()
1✔
696
    grp.add_argument(
1✔
697
        "-c",
698
        "--code",
699
        action="store_true",
700
        help="if provided, treat argument as a string of code",
701
    )
702
    grp.add_argument(
1✔
703
        "-n",
704
        "--load-namespace",
705
        action="store_true",
706
        help="if provided, treat argument as the name of a namespace",
707
    )
708

709
    parser.add_argument(
1✔
710
        "--in-ns",
711
        help="namespace to use for the code (default: basilisp.user); ignored when `-n` is used",
712
    )
713
    parser.add_argument(
1✔
714
        "args",
715
        nargs=argparse.REMAINDER,
716
        help="command line args made accessible to the script as basilisp.core/*command-line-args*",
717
    )
718
    _add_compiler_arg_group(parser)
1✔
719
    _add_import_arg_group(parser)
1✔
720
    _add_runtime_arg_group(parser)
1✔
721
    _add_debug_arg_group(parser)
1✔
722

723

724
def test(
725
    parser: argparse.ArgumentParser, args: argparse.Namespace
726
) -> None:  # pragma: no cover
727
    basilisp.init(_compiler_opts(args))
728
    try:
729
        import pytest
730
    except (ImportError, ModuleNotFoundError):
731
        parser.error(
732
            "Cannot run tests without dependency PyTest. Please install PyTest and try again.",
733
        )
734
    else:
735
        pytest.main(args=list(args.args))
736

737

738
@_subcommand(
1✔
739
    "test",
740
    help="run tests in a Basilisp project",
741
    description="Run tests in a Basilisp project.",
742
    handler=test,
743
)
744
def _add_test_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
745
    parser.add_argument("args", nargs=-1)
1✔
746
    _add_compiler_arg_group(parser)
1✔
747
    _add_runtime_arg_group(parser)
1✔
748
    _add_debug_arg_group(parser)
1✔
749

750

751
def version(_, __) -> None:
1✔
752
    v = importlib.metadata.version("basilisp")
1✔
753
    print(f"Basilisp {v}")
1✔
754

755

756
@_subcommand("version", help="print the version of Basilisp", handler=version)
1✔
757
def _add_version_subcommand(_: argparse.ArgumentParser) -> None:
1✔
758
    pass
1✔
759

760

761
def run_script():
1✔
762
    """Entrypoint to run the Basilisp script named by `sys.argv[1]` as by the
763
    `basilisp run` subcommand.
764

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

768
    The current process is replaced as by `os.execlp`."""
769
    # os.exec* functions do not perform shell expansion, so we must do so manually.
770
    script_path = Path(sys.argv[1]).resolve()
×
771
    args = ["basilisp", "run", str(script_path)]
×
772
    # Collect arguments sent to the script and pass them onto `basilisp run`
773
    if rest := sys.argv[2:]:
×
774
        args.append("--")
×
775
        args.extend(rest)
×
776
    os.execvp("basilisp", args)
×
777

778

779
def invoke_cli(args: Optional[Sequence[str]] = None) -> None:
1✔
780
    """Entrypoint to run the Basilisp CLI."""
781
    parser = argparse.ArgumentParser(
1✔
782
        description="Basilisp is a Lisp dialect inspired by Clojure targeting Python 3."
783
    )
784

785
    subparsers = parser.add_subparsers(help="sub-commands")
1✔
786
    _add_bootstrap_subcommand(subparsers)
1✔
787
    _add_nrepl_server_subcommand(subparsers)
1✔
788
    _add_repl_subcommand(subparsers)
1✔
789
    _add_run_subcommand(subparsers)
1✔
790
    _add_test_subcommand(subparsers)
1✔
791
    _add_version_subcommand(subparsers)
1✔
792

793
    parsed_args = parser.parse_args(args=args)
1✔
794
    if hasattr(parsed_args, "handler"):
1✔
795
        parsed_args.handler(parser, parsed_args)
1✔
796
    else:
797
        parser.print_help()
×
798

799

800
if __name__ == "__main__":
801
    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