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

basilisp-lang / basilisp / 14747779106

30 Apr 2025 06:01AM CUT coverage: 98.686%. Remained the same
14747779106

Pull #1255

github

web-flow
Merge 9607a0ba1 into cd51030f6
Pull Request #1255: suppress pytest rewrite assertion warning

1056 of 1065 branches covered (99.15%)

Branch coverage included in aggregate %.

9005 of 9130 relevant lines covered (98.63%)

0.99 hits per line

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

95.86
/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 collections.abc import Sequence
1✔
10
from pathlib import Path
1✔
11
from typing import Any, Callable, Optional, Union
1✔
12

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

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

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

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

39

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

48

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

57

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

65

66
def bootstrap_repl(ctx: compiler.CompilerContext, which_ns: str) -> types.ModuleType:
1✔
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."""
69
    which_ns_sym = sym.symbol(which_ns)
1✔
70
    ns = runtime.Namespace.get_or_create(which_ns_sym)
1✔
71
    compiler.compile_and_exec_form(
1✔
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
    )
80
    return importlib.import_module(REPL_NS)
1✔
81

82

83
def init_path(args: argparse.Namespace, unsafe_path: str = "") -> None:
1✔
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

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

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

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

99

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

111

112
def _set_envvar_action(
1✔
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

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

128
    return EnvVarSetterAction
1✔
129

130

131
def _add_compiler_arg_group(parser: argparse.ArgumentParser) -> None:
1✔
132
    group = parser.add_argument_group(
1✔
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
    )
142
    group.add_argument(
1✔
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
    )
155
    group.add_argument(
1✔
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
    )
168
    group.add_argument(
1✔
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
    )
181
    group.add_argument(
1✔
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
    )
193
    group.add_argument(
1✔
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
    )
205
    group.add_argument(
1✔
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
    )
217
    group.add_argument(
1✔
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
    )
230
    group.add_argument(
1✔
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
    )
242
    group.add_argument(
1✔
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

256
def _compiler_opts(args: argparse.Namespace) -> CompilerOpts:
1✔
257
    return compiler.compiler_opts(
1✔
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

270
def _add_debug_arg_group(parser: argparse.ArgumentParser) -> None:
1✔
271
    group = parser.add_argument_group("debug options")
1✔
272
    group.add_argument(
1✔
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
    )
285
    group.add_argument(
1✔
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
    )
298
    group.add_argument(
1✔
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
    )
311
    group.add_argument(
1✔
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

327
def _add_import_arg_group(parser: argparse.ArgumentParser) -> None:
1✔
328
    group = parser.add_argument_group(
1✔
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
    )
335
    group.add_argument(
1✔
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
    )
349
    group.add_argument(
1✔
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

360
def _add_runtime_arg_group(parser: argparse.ArgumentParser) -> None:
1✔
361
    group = parser.add_argument_group(
1✔
362
        "runtime arguments",
363
        description=(
364
            "The runtime arguments below affect reader and execution time features."
365
        ),
366
    )
367
    group.add_argument(
1✔
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

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

388

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

411
        return _wrapped_subcommand
1✔
412

413
    return _wrap_add_subcommand
1✔
414

415

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

422
    if args.uninstall:
1✔
423
        if not (
1✔
424
            removed := basilisp.unbootstrap_python(site_packages=args.site_packages)
425
        ):
426
            print_("No Basilisp bootstrap files were found.")
1✔
427
        else:
428
            if removed is not None:
1✔
429
                print_(f"Removed '{removed}'")
1✔
430
    else:
431
        path = basilisp.bootstrap_python(site_packages=args.site_packages)
1✔
432
        print_(
1✔
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

439
@_subcommand(
1✔
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
)
461
def _add_bootstrap_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
462
    parser.add_argument(
1✔
463
        "--uninstall",
464
        action="store_true",
465
        help="if true, remove any `.pth` files installed by Basilisp in all site-packages directories",
466
    )
467
    parser.add_argument(
1✔
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.
475
    parser.add_argument(
1✔
476
        "--site-packages",
477
        help=argparse.SUPPRESS,
478
    )
479

480

481
def nrepl_server(
1✔
482
    _,
483
    args: argparse.Namespace,
484
) -> None:
485
    basilisp.init(_compiler_opts(args))
1✔
486
    init_path(args)
1✔
487
    nrepl_server_mod = importlib.import_module(munge(NREPL_SERVER_NS))
1✔
488
    nrepl_server_mod.start_server__BANG__(
1✔
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

499
@_subcommand(
1✔
500
    "nrepl-server",
501
    help="start the nREPL server",
502
    description="Start the nREPL server.",
503
    handler=nrepl_server,
504
)
505
def _add_nrepl_server_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
506
    parser.add_argument(
1✔
507
        "--host",
508
        default="127.0.0.1",
509
        help="the interface address to bind to, defaults to 127.0.0.1.",
510
    )
511
    parser.add_argument(
1✔
512
        "--port",
513
        default=0,
514
        type=int,
515
        help="the port to connect to, defaults to 0 (random available port).",
516
    )
517
    parser.add_argument(
1✔
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
    )
522
    _add_compiler_arg_group(parser)
1✔
523
    _add_import_arg_group(parser)
1✔
524
    _add_runtime_arg_group(parser)
1✔
525
    _add_debug_arg_group(parser)
1✔
526

527

528
def repl(
1✔
529
    _,
530
    args: argparse.Namespace,
531
) -> None:
532
    opts = _compiler_opts(args)
1✔
533
    basilisp.init(opts)
1✔
534
    init_path(args)
1✔
535
    ctx = compiler.CompilerContext(filename=REPL_INPUT_FILE_PATH, opts=opts)
1✔
536
    prompter = get_prompter()
1✔
537
    eof = object()
1✔
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`.
541
    with runtime.bindings(
1✔
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
    ):
566
        repl_module = bootstrap_repl(ctx, args.default_ns)
1✔
567
        ns_var = runtime.set_current_ns(args.default_ns)
1✔
568

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

579
            if len(lsrc) == 0:
1✔
580
                continue
1✔
581

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

601

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

619

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

634
    opts = _compiler_opts(args)
1✔
635
    basilisp.init(opts)
1✔
636
    ctx = compiler.CompilerContext(
1✔
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
    )
644
    eof = object()
1✔
645

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

649
    with runtime.ns_bindings(in_ns) as ns:
1✔
650
        ns.refer_all(core_ns)
1✔
651

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

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

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

675

676
@_subcommand(
1✔
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
)
692
def _add_run_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
693
    parser.add_argument(
1✔
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

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

715
    parser.add_argument(
1✔
716
        "--in-ns",
717
        help="namespace to use for the code (default: basilisp.user); ignored when `-n` is used",
718
    )
719
    parser.add_argument(
1✔
720
        "args",
721
        nargs=argparse.REMAINDER,
722
        help="command line args made accessible to the script as basilisp.core/*command-line-args*",
723
    )
724
    _add_compiler_arg_group(parser)
1✔
725
    _add_import_arg_group(parser)
1✔
726
    _add_runtime_arg_group(parser)
1✔
727
    _add_debug_arg_group(parser)
1✔
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.
752
        extra = [
753
            "-W",
754
            "ignore:Module already imported so cannot be rewritten:pytest.PytestAssertRewriteWarning",
755
        ] + extra
756

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

759

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

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

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

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

775
            `basilisp test -- -h`
776

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

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

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

793

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

798

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

803

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

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

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

821

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

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

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

848

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