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

basilisp-lang / basilisp / 21098738927

17 Jan 2026 06:12PM UTC coverage: 71.67% (-27.0%) from 98.7%
21098738927

Pull #1308

github

web-flow
Merge bfa19f2a1 into edd610f2f
Pull Request #1308: Revert support for `clojure.test` style test fixtures

707 of 1084 branches covered (65.22%)

Branch coverage included in aggregate %.

1 of 2 new or added lines in 1 file covered. (50.0%)

2415 existing lines in 36 files now uncovered.

6675 of 9216 relevant lines covered (72.43%)

0.72 hits per line

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

0.0
/src/basilisp/cli.py
UNCOV
1
import argparse
×
UNCOV
2
import importlib.metadata
×
UNCOV
3
import io
×
UNCOV
4
import os
×
UNCOV
5
import pathlib
×
UNCOV
6
import sys
×
UNCOV
7
import textwrap
×
UNCOV
8
import types
×
UNCOV
9
from collections.abc import Callable, Sequence
×
UNCOV
10
from pathlib import Path
×
UNCOV
11
from typing import Any, Union
×
12

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

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

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

UNCOV
37
DEFAULT_COMPILER_OPTS = {k.name: v for k, v in compiler.compiler_opts().items()}
×
38

39

UNCOV
40
def eval_stream(stream, ctx: compiler.CompilerContext, ns: runtime.Namespace):
×
41
    """Evaluate the forms in stdin into a Python module AST node."""
UNCOV
42
    last = None
×
UNCOV
43
    for form in reader.read(stream, resolver=runtime.resolve_alias):
×
UNCOV
44
        assert not isinstance(form, reader.ReaderConditional)
×
UNCOV
45
        last = compiler.compile_and_exec_form(form, ctx, ns)
×
UNCOV
46
    return last
×
47

48

UNCOV
49
def eval_str(s: str, ctx: compiler.CompilerContext, ns: runtime.Namespace, eof: Any):
×
50
    """Evaluate the forms in a string into a Python module AST node."""
UNCOV
51
    last = eof
×
UNCOV
52
    for form in reader.read_str(s, resolver=runtime.resolve_alias, eof=eof):
×
UNCOV
53
        assert not isinstance(form, reader.ReaderConditional)
×
UNCOV
54
        last = compiler.compile_and_exec_form(form, ctx, ns)
×
UNCOV
55
    return last
×
56

57

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

65

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

82

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

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

UNCOV
92
    for pth in args.include_path or []:
×
UNCOV
93
        p = pathlib.Path(pth).resolve()
×
UNCOV
94
        prepend_once(str(p))
×
95

UNCOV
96
    if args.include_unsafe_path:
×
UNCOV
97
        prepend_once(unsafe_path)
×
98

99

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

111

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

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

UNCOV
128
    return EnvVarSetterAction
×
129

130

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

255

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

269

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

326

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

359

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

382

UNCOV
383
Handler = Union[
×
384
    Callable[[argparse.ArgumentParser, argparse.Namespace], None],
385
    Callable[[argparse.ArgumentParser, argparse.Namespace, list[str]], None],
386
]
387

388

UNCOV
389
def _subcommand(
×
390
    subcommand: str,
391
    *,
392
    help: str | None = None,  # pylint: disable=redefined-builtin
393
    description: str | None = None,
394
    handler: Handler,
395
    allows_extra: bool = False,
396
) -> Callable[
397
    [Callable[[argparse.ArgumentParser], None]],
398
    Callable[["argparse._SubParsersAction"], None],
399
]:
UNCOV
400
    def _wrap_add_subcommand(
×
401
        f: Callable[[argparse.ArgumentParser], None],
402
    ) -> Callable[["argparse._SubParsersAction"], None]:
UNCOV
403
        def _wrapped_subcommand(subparsers: "argparse._SubParsersAction"):
×
UNCOV
404
            parser = subparsers.add_parser(
×
405
                subcommand, help=help, description=description
406
            )
UNCOV
407
            parser.set_defaults(handler=handler)
×
UNCOV
408
            parser.set_defaults(allows_extra=allows_extra)
×
UNCOV
409
            f(parser)
×
410

UNCOV
411
        return _wrapped_subcommand
×
412

UNCOV
413
    return _wrap_add_subcommand
×
414

415

UNCOV
416
def bootstrap_basilisp_installation(_, args: argparse.Namespace) -> None:
×
UNCOV
417
    if args.quiet:
×
UNCOV
418
        print_ = lambda v: v
×
419
    else:
UNCOV
420
        print_ = print
×
421

UNCOV
422
    if args.uninstall:
×
UNCOV
423
        if not (
×
424
            removed := basilisp.unbootstrap_python(site_packages=args.site_packages)
425
        ):
UNCOV
426
            print_("No Basilisp bootstrap files were found.")
×
427
        else:
UNCOV
428
            if removed is not None:
×
UNCOV
429
                print_(f"Removed '{removed}'")
×
430
    else:
UNCOV
431
        path = basilisp.bootstrap_python(site_packages=args.site_packages)
×
UNCOV
432
        print_(
×
433
            f"(Added {path})\n\n"
434
            "Your Python installation has been bootstrapped! You can undo this at any "
435
            "time with with `basilisp bootstrap --uninstall`."
436
        )
437

438

UNCOV
439
@_subcommand(
×
440
    "bootstrap",
441
    help="bootstrap the Python installation to allow importing Basilisp namespaces",
442
    description=textwrap.dedent(
443
        """Bootstrap the Python installation to allow importing Basilisp namespaces"
444
        without requiring an additional bootstrapping step.
445

446
        Python installations are bootstrapped by installing a `basilispbootstrap.pth`
447
        file in your `site-packages` directory. Python installations execute `*.pth`
448
        files found at startup.
449

450
        Bootstrapping your Python installation in this way can help avoid needing to
451
        perform manual bootstrapping from Python code within your application.
452

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

480

UNCOV
481
def nrepl_server(
×
482
    _,
483
    args: argparse.Namespace,
484
) -> None:
UNCOV
485
    basilisp.init(_compiler_opts(args))
×
UNCOV
486
    init_path(args)
×
UNCOV
487
    nrepl_server_mod = importlib.import_module(munge(NREPL_SERVER_NS))
×
UNCOV
488
    nrepl_server_mod.start_server__BANG__(
×
489
        lmap.map(
490
            {
491
                kw.keyword("host"): args.host,
492
                kw.keyword("port"): args.port,
493
                kw.keyword("nrepl-port-file"): args.port_filepath,
494
            }
495
        )
496
    )
497

498

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

527

UNCOV
528
def repl(
×
529
    _,
530
    args: argparse.Namespace,
531
) -> None:
UNCOV
532
    opts = _compiler_opts(args)
×
UNCOV
533
    basilisp.init(opts)
×
UNCOV
534
    init_path(args)
×
UNCOV
535
    ctx = compiler.CompilerContext(filename=REPL_INPUT_FILE_PATH, opts=opts)
×
UNCOV
536
    prompter = get_prompter()
×
UNCOV
537
    eof = object()
×
538

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

UNCOV
569
        while True:
×
UNCOV
570
            ns: runtime.Namespace = ns_var.value
×
UNCOV
571
            try:
×
UNCOV
572
                lsrc = prompter.prompt(f"{ns.name}=> ")
×
UNCOV
573
            except EOFError:
×
UNCOV
574
                break
×
575
            except KeyboardInterrupt:  # pragma: no cover
576
                print("")
577
                continue
578

UNCOV
579
            if len(lsrc) == 0:
×
UNCOV
580
                continue
×
581

UNCOV
582
            try:
×
UNCOV
583
                result = eval_str(lsrc, ctx, ns, eof)
×
584
                if result is eof:  # pragma: no cover
585
                    continue
UNCOV
586
                prompter.print(runtime.lrepr(result))
×
UNCOV
587
                repl_module.mark_repl_result(result)
×
UNCOV
588
            except reader.SyntaxError as e:
×
UNCOV
589
                print_exception(e, reader.SyntaxError, e.__traceback__)
×
UNCOV
590
                repl_module.mark_exception(e)
×
UNCOV
591
                continue
×
UNCOV
592
            except compiler.CompilerException as e:
×
UNCOV
593
                print_exception(e, compiler.CompilerException, e.__traceback__)
×
UNCOV
594
                repl_module.mark_exception(e)
×
UNCOV
595
                continue
×
UNCOV
596
            except Exception as e:  # pylint: disable=broad-exception-caught
×
UNCOV
597
                print_exception(e, Exception, e.__traceback__)
×
UNCOV
598
                repl_module.mark_exception(e)
×
UNCOV
599
                continue
×
600

601

UNCOV
602
@_subcommand(
×
603
    "repl",
604
    help="start the Basilisp REPL",
605
    description="Start a Basilisp REPL.",
606
    handler=repl,
607
)
UNCOV
608
def _add_repl_subcommand(parser: argparse.ArgumentParser) -> None:
×
UNCOV
609
    parser.add_argument(
×
610
        "--default-ns",
611
        default=runtime.REPL_DEFAULT_NS,
612
        help="default namespace to use for the REPL",
613
    )
UNCOV
614
    _add_compiler_arg_group(parser)
×
UNCOV
615
    _add_import_arg_group(parser)
×
UNCOV
616
    _add_runtime_arg_group(parser)
×
UNCOV
617
    _add_debug_arg_group(parser)
×
618

619

UNCOV
620
def run(
×
621
    parser: argparse.ArgumentParser,
622
    args: argparse.Namespace,
623
) -> None:
UNCOV
624
    target = args.file_or_ns_or_code
×
UNCOV
625
    if args.load_namespace:
×
UNCOV
626
        if args.in_ns is not None:
×
UNCOV
627
            parser.error(
×
628
                "argument --in-ns: not allowed with argument -n/--load-namespace"
629
            )
UNCOV
630
        in_ns = runtime.REPL_DEFAULT_NS
×
631
    else:
UNCOV
632
        in_ns = target if args.in_ns is not None else runtime.REPL_DEFAULT_NS
×
633

UNCOV
634
    opts = _compiler_opts(args)
×
UNCOV
635
    basilisp.init(opts)
×
UNCOV
636
    ctx = compiler.CompilerContext(
×
637
        filename=(
638
            CLI_INPUT_FILE_PATH
639
            if args.code
640
            else (STDIN_INPUT_FILE_PATH if target == STDIN_FILE_NAME else target)
641
        ),
642
        opts=opts,
643
    )
UNCOV
644
    eof = object()
×
645

UNCOV
646
    core_ns = runtime.Namespace.get(runtime.CORE_NS_SYM)
×
UNCOV
647
    assert core_ns is not None
×
648

UNCOV
649
    with runtime.ns_bindings(in_ns) as ns:
×
UNCOV
650
        ns.refer_all(core_ns)
×
651

UNCOV
652
        if args.args:
×
UNCOV
653
            cli_args_var = core_ns.find(sym.symbol(runtime.COMMAND_LINE_ARGS_VAR_NAME))
×
UNCOV
654
            assert cli_args_var is not None
×
UNCOV
655
            cli_args_var.bind_root(vec.vector(args.args))
×
656

UNCOV
657
        if args.code:
×
UNCOV
658
            init_path(args)
×
UNCOV
659
            eval_str(target, ctx, ns, eof)
×
UNCOV
660
        elif args.load_namespace:
×
661
            # Set the requested namespace as the *main-ns*
UNCOV
662
            main_ns_var = core_ns.find(sym.symbol(runtime.MAIN_NS_VAR_NAME))
×
UNCOV
663
            assert main_ns_var is not None
×
UNCOV
664
            main_ns_var.bind_root(sym.symbol(target))
×
665

UNCOV
666
            init_path(args)
×
UNCOV
667
            importlib.import_module(munge(target))
×
UNCOV
668
        elif target == STDIN_FILE_NAME:
×
UNCOV
669
            init_path(args)
×
UNCOV
670
            eval_stream(io.TextIOWrapper(sys.stdin.buffer, encoding="utf-8"), ctx, ns)
×
671
        else:
UNCOV
672
            init_path(args, unsafe_path=str(pathlib.Path(target).resolve().parent))
×
UNCOV
673
            eval_file(target, ctx, ns)
×
674

675

UNCOV
676
@_subcommand(
×
677
    "run",
678
    help="run a Basilisp script or code or namespace",
679
    description=textwrap.dedent(
680
        """Run a Basilisp script or a line of code or load a Basilisp namespace.
681

682
        If `-c` is provided, execute the line of code as given. If `-n` is given,
683
        interpret `file_or_ns_or_code` as a fully qualified Basilisp namespace
684
        relative to `sys.path`. Otherwise, execute the file as a script relative to
685
        the current working directory.
686

687
        `*main-ns*` will be set to the value provided for `-n`. In all other cases,
688
        it will be `nil`."""
689
    ),
690
    handler=run,
691
)
UNCOV
692
def _add_run_subcommand(parser: argparse.ArgumentParser) -> None:
×
UNCOV
693
    parser.add_argument(
×
694
        "file_or_ns_or_code",
695
        help=(
696
            "file path to a Basilisp file, a string of Basilisp code, or a fully "
697
            "qualified Basilisp namespace name"
698
        ),
699
    )
700

UNCOV
701
    grp = parser.add_mutually_exclusive_group()
×
UNCOV
702
    grp.add_argument(
×
703
        "-c",
704
        "--code",
705
        action="store_true",
706
        help="if provided, treat argument as a string of code",
707
    )
UNCOV
708
    grp.add_argument(
×
709
        "-n",
710
        "--load-namespace",
711
        action="store_true",
712
        help="if provided, treat argument as the name of a namespace",
713
    )
714

UNCOV
715
    parser.add_argument(
×
716
        "--in-ns",
717
        help="namespace to use for the code (default: basilisp.user); ignored when `-n` is used",
718
    )
UNCOV
719
    parser.add_argument(
×
720
        "args",
721
        nargs=argparse.REMAINDER,
722
        help="command line args made accessible to the script as basilisp.core/*command-line-args*",
723
    )
UNCOV
724
    _add_compiler_arg_group(parser)
×
UNCOV
725
    _add_import_arg_group(parser)
×
UNCOV
726
    _add_runtime_arg_group(parser)
×
UNCOV
727
    _add_debug_arg_group(parser)
×
728

729

730
def test(
731
    parser: argparse.ArgumentParser,
732
    args: argparse.Namespace,
733
    extra: list[str],
734
) -> None:  # pragma: no cover
735
    init_path(args)
736
    basilisp.init(_compiler_opts(args))
737
    # parse_known_args leaves the `--` separator as the first element if it is present
738
    # but retaining that causes PyTest to interpret all the arguments as positional
739
    if extra and extra[0] == "--":
740
        extra = extra[1:]
741
    try:
742
        import pytest
743
    except (ImportError, ModuleNotFoundError):
744
        parser.error(
745
            "Cannot run tests without dependency PyTest. Please install PyTest and try again.",
746
        )
747
    else:
748
        # `basilisp` declares the testrunner as a pytest plugin, so
749
        # pytest tries to import it for assertion rewriting.  Since
750
        # it's already imported, pytest emits a warning. As rewriting
751
        # isn't needed, we ignore it. (Requires pytest >=8.4.0 to take
752
        # effect)
753
        extra = [
754
            "-W",
755
            "ignore:Module already imported so cannot be rewritten; basilisp:pytest.PytestAssertRewriteWarning",
756
        ] + extra
757

758
        sys.exit(pytest.main(args=list(extra)))
759

760

UNCOV
761
@_subcommand(
×
762
    "test",
763
    help="run tests in a Basilisp project",
764
    description=textwrap.dedent(
765
        """Run tests in a Basilisp project.
766

767
        Any options not recognized by Basilisp and all positional arguments will
768
        be collected and passed on to PyTest. It is possible to directly signal
769
        the end of option processing using an explicit `--` as in:
770

771
            `basilisp test -p other_dir -- -k vector`
772

773
        This can be useful to also directly execute PyTest commands with Basilisp.
774
        For instance, you can directly print the PyTest command-line help text using:
775

776
            `basilisp test -- -h`
777

778
        If all options are unambiguous (e.g. they are only either used by Basilisp
779
        or by PyTest), then you can omit the `--`:
780

781
            `basilisp test -k vector -p other_dir`
782

783
        Returns the PyTest exit code as the exit code."""
784
    ),
785
    handler=test,
786
    allows_extra=True,
787
)
UNCOV
788
def _add_test_subcommand(parser: argparse.ArgumentParser) -> None:
×
UNCOV
789
    _add_compiler_arg_group(parser)
×
UNCOV
790
    _add_import_arg_group(parser)
×
UNCOV
791
    _add_runtime_arg_group(parser)
×
UNCOV
792
    _add_debug_arg_group(parser)
×
793

794

UNCOV
795
def version(_, __) -> None:
×
UNCOV
796
    v = importlib.metadata.version("basilisp")
×
UNCOV
797
    print(f"Basilisp {v}")
×
798

799

UNCOV
800
@_subcommand("version", help="print the version of Basilisp", handler=version)
×
UNCOV
801
def _add_version_subcommand(_: argparse.ArgumentParser) -> None:
×
UNCOV
802
    pass
×
803

804

UNCOV
805
def run_script():
×
806
    """Entrypoint to run the Basilisp script named by `sys.argv[1]` as by the
807
    `basilisp run` subcommand.
808

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

812
    The current process is replaced as by `os.execvp`."""
813
    # os.exec* functions do not perform shell expansion, so we must do so manually.
814
    script_path = Path(sys.argv[1]).resolve()
×
815
    args = ["basilisp", "run", str(script_path)]
×
816
    # Collect arguments sent to the script and pass them onto `basilisp run`
817
    if rest := sys.argv[2:]:
×
818
        args.append("--")
×
819
        args.extend(rest)
×
820
    os.execvp("basilisp", args)  # nosec B606, B607
×
821

822

UNCOV
823
def invoke_cli(args: Sequence[str] | None = None) -> None:
×
824
    """Entrypoint to run the Basilisp CLI."""
UNCOV
825
    parser = argparse.ArgumentParser(
×
826
        description="Basilisp is a Lisp dialect inspired by Clojure targeting Python 3."
827
    )
828

UNCOV
829
    subparsers = parser.add_subparsers(help="sub-commands")
×
UNCOV
830
    _add_bootstrap_subcommand(subparsers)
×
UNCOV
831
    _add_nrepl_server_subcommand(subparsers)
×
UNCOV
832
    _add_repl_subcommand(subparsers)
×
UNCOV
833
    _add_run_subcommand(subparsers)
×
UNCOV
834
    _add_test_subcommand(subparsers)
×
UNCOV
835
    _add_version_subcommand(subparsers)
×
836

UNCOV
837
    parsed_args, extra = parser.parse_known_args(args=args)
×
UNCOV
838
    allows_extra = getattr(parsed_args, "allows_extra", False)
×
UNCOV
839
    if extra and not allows_extra:
×
840
        parser.error(f"unrecognized arguments: {' '.join(extra)}")
×
UNCOV
841
    elif hasattr(parsed_args, "handler"):
×
UNCOV
842
        if allows_extra:
×
843
            parsed_args.handler(parser, parsed_args, extra)
×
844
        else:
UNCOV
845
            parsed_args.handler(parser, parsed_args)
×
846
    else:
847
        parser.print_help()
×
848

849

850
if __name__ == "__main__":
851
    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

© 2026 Coveralls, Inc