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

basilisp-lang / basilisp / 14563463011

20 Apr 2025 09:26PM UTC coverage: 98.496% (-0.2%) from 98.679%
14563463011

Pull #1243

github

web-flow
Merge 137f990be into 0fead055c
Pull Request #1243: Add a CLI subcommand to install Clojure Jar files in the virtualenv

1047 of 1060 branches covered (98.77%)

Branch coverage included in aggregate %.

13 of 28 new or added lines in 2 files covered. (46.43%)

8971 of 9111 relevant lines covered (98.46%)

0.98 hits per line

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

94.37
/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 sysconfig
1✔
8
import textwrap
1✔
9
import types
1✔
10
from collections.abc import Sequence
1✔
11
from pathlib import Path
1✔
12
from typing import Any, Callable, Optional, Union
1✔
13

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

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

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

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

41

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

50

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

59

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

67

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

84

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

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

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

98
    if args.include_unsafe_path:
1✔
99
        prepend_once(unsafe_path)
1✔
100

101

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

113

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

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

130
    return EnvVarSetterAction
1✔
131

132

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

257

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

271

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

328

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

361

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

384

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

390

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

413
        return _wrapped_subcommand
1✔
414

415
    return _wrap_add_subcommand
1✔
416

417

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

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

440

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

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

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

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

482

483
def install_jar(_, args: argparse.Namespace) -> None:
1✔
NEW
484
    jars = args.jars
×
NEW
485
    site_packages = sysconfig.get_paths()["purelib"]
×
486

NEW
487
    for jar in jars:
×
NEW
488
        jarutil.install_jar(Path(site_packages), Path(jar))
×
489

490

491
@_subcommand(
1✔
492
    "install-jar",
493
    help="install a JAR file into the virtualenv",
494
    description=textwrap.dedent(
495
        """Install a JAR file from your local Maven repository into the current
496
        virtualenv."""
497
    ),
498
    handler=install_jar,
499
)
500
def _add_install_jar_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
501
    parser.add_argument(
1✔
502
        "jars", nargs="*", help="one or more JAR files from a local filesystem location"
503
    )
504

505

506
def nrepl_server(
1✔
507
    _,
508
    args: argparse.Namespace,
509
) -> None:
510
    basilisp.init(_compiler_opts(args))
1✔
511
    init_path(args)
1✔
512
    nrepl_server_mod = importlib.import_module(munge(NREPL_SERVER_NS))
1✔
513
    nrepl_server_mod.start_server__BANG__(
1✔
514
        lmap.map(
515
            {
516
                kw.keyword("host"): args.host,
517
                kw.keyword("port"): args.port,
518
                kw.keyword("nrepl-port-file"): args.port_filepath,
519
            }
520
        )
521
    )
522

523

524
@_subcommand(
1✔
525
    "nrepl-server",
526
    help="start the nREPL server",
527
    description="Start the nREPL server.",
528
    handler=nrepl_server,
529
)
530
def _add_nrepl_server_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
531
    parser.add_argument(
1✔
532
        "--host",
533
        default="127.0.0.1",
534
        help="the interface address to bind to, defaults to 127.0.0.1.",
535
    )
536
    parser.add_argument(
1✔
537
        "--port",
538
        default=0,
539
        type=int,
540
        help="the port to connect to, defaults to 0 (random available port).",
541
    )
542
    parser.add_argument(
1✔
543
        "--port-filepath",
544
        default=".nrepl-port",
545
        help='the file path where the server port number is output to, defaults to ".nrepl-port".',
546
    )
547
    _add_compiler_arg_group(parser)
1✔
548
    _add_import_arg_group(parser)
1✔
549
    _add_runtime_arg_group(parser)
1✔
550
    _add_debug_arg_group(parser)
1✔
551

552

553
def repl(
1✔
554
    _,
555
    args: argparse.Namespace,
556
) -> None:
557
    opts = _compiler_opts(args)
1✔
558
    basilisp.init(opts)
1✔
559
    init_path(args)
1✔
560
    ctx = compiler.CompilerContext(filename=REPL_INPUT_FILE_PATH, opts=opts)
1✔
561
    prompter = get_prompter()
1✔
562
    eof = object()
1✔
563

564
    # Bind user-settable dynamic Vars to their existing value to allow users to
565
    # conveniently (set! *var* val) at the REPL without needing `binding`.
566
    with runtime.bindings(
1✔
567
        {
568
            var: var.value
569
            for var in map(
570
                lambda name: runtime.Var.find_safe(
571
                    sym.symbol(name, ns=runtime.CORE_NS)
572
                ),
573
                [
574
                    "*e",
575
                    "*1",
576
                    "*2",
577
                    "*3",
578
                    "*assert*",
579
                    "*data-readers*",
580
                    "*resolver*",
581
                    runtime.PRINT_DUP_VAR_NAME,
582
                    runtime.PRINT_LEVEL_VAR_NAME,
583
                    runtime.PRINT_READABLY_VAR_NAME,
584
                    runtime.PRINT_LEVEL_VAR_NAME,
585
                    runtime.PRINT_META_VAR_NAME,
586
                    runtime.PRINT_NAMESPACE_MAPS_VAR_NAME,
587
                ],
588
            )
589
        }
590
    ):
591
        repl_module = bootstrap_repl(ctx, args.default_ns)
1✔
592
        ns_var = runtime.set_current_ns(args.default_ns)
1✔
593

594
        while True:
1✔
595
            ns: runtime.Namespace = ns_var.value
1✔
596
            try:
1✔
597
                lsrc = prompter.prompt(f"{ns.name}=> ")
1✔
598
            except EOFError:
1✔
599
                break
1✔
600
            except KeyboardInterrupt:  # pragma: no cover
601
                print("")
602
                continue
603

604
            if len(lsrc) == 0:
1✔
605
                continue
1✔
606

607
            try:
1✔
608
                result = eval_str(lsrc, ctx, ns, eof)
1✔
609
                if result is eof:  # pragma: no cover
610
                    continue
611
                prompter.print(runtime.lrepr(result))
1✔
612
                repl_module.mark_repl_result(result)
1✔
613
            except reader.SyntaxError as e:
1✔
614
                print_exception(e, reader.SyntaxError, e.__traceback__)
1✔
615
                repl_module.mark_exception(e)
1✔
616
                continue
1✔
617
            except compiler.CompilerException as e:
1✔
618
                print_exception(e, compiler.CompilerException, e.__traceback__)
1✔
619
                repl_module.mark_exception(e)
1✔
620
                continue
1✔
621
            except Exception as e:  # pylint: disable=broad-exception-caught
1✔
622
                print_exception(e, Exception, e.__traceback__)
1✔
623
                repl_module.mark_exception(e)
1✔
624
                continue
1✔
625

626

627
@_subcommand(
1✔
628
    "repl",
629
    help="start the Basilisp REPL",
630
    description="Start a Basilisp REPL.",
631
    handler=repl,
632
)
633
def _add_repl_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
634
    parser.add_argument(
1✔
635
        "--default-ns",
636
        default=runtime.REPL_DEFAULT_NS,
637
        help="default namespace to use for the REPL",
638
    )
639
    _add_compiler_arg_group(parser)
1✔
640
    _add_import_arg_group(parser)
1✔
641
    _add_runtime_arg_group(parser)
1✔
642
    _add_debug_arg_group(parser)
1✔
643

644

645
def run(
1✔
646
    parser: argparse.ArgumentParser,
647
    args: argparse.Namespace,
648
) -> None:
649
    target = args.file_or_ns_or_code
1✔
650
    if args.load_namespace:
1✔
651
        if args.in_ns is not None:
1✔
652
            parser.error(
1✔
653
                "argument --in-ns: not allowed with argument -n/--load-namespace"
654
            )
655
        in_ns = runtime.REPL_DEFAULT_NS
1✔
656
    else:
657
        in_ns = target if args.in_ns is not None else runtime.REPL_DEFAULT_NS
1✔
658

659
    opts = _compiler_opts(args)
1✔
660
    basilisp.init(opts)
1✔
661
    ctx = compiler.CompilerContext(
1✔
662
        filename=(
663
            CLI_INPUT_FILE_PATH
664
            if args.code
665
            else (STDIN_INPUT_FILE_PATH if target == STDIN_FILE_NAME else target)
666
        ),
667
        opts=opts,
668
    )
669
    eof = object()
1✔
670

671
    core_ns = runtime.Namespace.get(runtime.CORE_NS_SYM)
1✔
672
    assert core_ns is not None
1✔
673

674
    with runtime.ns_bindings(in_ns) as ns:
1✔
675
        ns.refer_all(core_ns)
1✔
676

677
        if args.args:
1✔
678
            cli_args_var = core_ns.find(sym.symbol(runtime.COMMAND_LINE_ARGS_VAR_NAME))
1✔
679
            assert cli_args_var is not None
1✔
680
            cli_args_var.bind_root(vec.vector(args.args))
1✔
681

682
        if args.code:
1✔
683
            init_path(args)
1✔
684
            eval_str(target, ctx, ns, eof)
1✔
685
        elif args.load_namespace:
1✔
686
            # Set the requested namespace as the *main-ns*
687
            main_ns_var = core_ns.find(sym.symbol(runtime.MAIN_NS_VAR_NAME))
1✔
688
            assert main_ns_var is not None
1✔
689
            main_ns_var.bind_root(sym.symbol(target))
1✔
690

691
            init_path(args)
1✔
692
            importlib.import_module(munge(target))
1✔
693
        elif target == STDIN_FILE_NAME:
1✔
694
            init_path(args)
1✔
695
            eval_stream(io.TextIOWrapper(sys.stdin.buffer, encoding="utf-8"), ctx, ns)
1✔
696
        else:
697
            init_path(args, unsafe_path=str(pathlib.Path(target).resolve().parent))
1✔
698
            eval_file(target, ctx, ns)
1✔
699

700

701
@_subcommand(
1✔
702
    "run",
703
    help="run a Basilisp script or code or namespace",
704
    description=textwrap.dedent(
705
        """Run a Basilisp script or a line of code or load a Basilisp namespace.
706

707
        If `-c` is provided, execute the line of code as given. If `-n` is given,
708
        interpret `file_or_ns_or_code` as a fully qualified Basilisp namespace
709
        relative to `sys.path`. Otherwise, execute the file as a script relative to
710
        the current working directory.
711

712
        `*main-ns*` will be set to the value provided for `-n`. In all other cases,
713
        it will be `nil`."""
714
    ),
715
    handler=run,
716
)
717
def _add_run_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
718
    parser.add_argument(
1✔
719
        "file_or_ns_or_code",
720
        help=(
721
            "file path to a Basilisp file, a string of Basilisp code, or a fully "
722
            "qualified Basilisp namespace name"
723
        ),
724
    )
725

726
    grp = parser.add_mutually_exclusive_group()
1✔
727
    grp.add_argument(
1✔
728
        "-c",
729
        "--code",
730
        action="store_true",
731
        help="if provided, treat argument as a string of code",
732
    )
733
    grp.add_argument(
1✔
734
        "-n",
735
        "--load-namespace",
736
        action="store_true",
737
        help="if provided, treat argument as the name of a namespace",
738
    )
739

740
    parser.add_argument(
1✔
741
        "--in-ns",
742
        help="namespace to use for the code (default: basilisp.user); ignored when `-n` is used",
743
    )
744
    parser.add_argument(
1✔
745
        "args",
746
        nargs=argparse.REMAINDER,
747
        help="command line args made accessible to the script as basilisp.core/*command-line-args*",
748
    )
749
    _add_compiler_arg_group(parser)
1✔
750
    _add_import_arg_group(parser)
1✔
751
    _add_runtime_arg_group(parser)
1✔
752
    _add_debug_arg_group(parser)
1✔
753

754

755
def test(
756
    parser: argparse.ArgumentParser,
757
    args: argparse.Namespace,
758
    extra: list[str],
759
) -> None:  # pragma: no cover
760
    init_path(args)
761
    basilisp.init(_compiler_opts(args))
762
    # parse_known_args leaves the `--` separator as the first element if it is present
763
    # but retaining that causes PyTest to interpret all the arguments as positional
764
    if extra and extra[0] == "--":
765
        extra = extra[1:]
766
    try:
767
        import pytest
768
    except (ImportError, ModuleNotFoundError):
769
        parser.error(
770
            "Cannot run tests without dependency PyTest. Please install PyTest and try again.",
771
        )
772
    else:
773
        sys.exit(pytest.main(args=list(extra)))
774

775

776
@_subcommand(
1✔
777
    "test",
778
    help="run tests in a Basilisp project",
779
    description=textwrap.dedent(
780
        """Run tests in a Basilisp project.
781

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

786
            `basilisp test -p other_dir -- -k vector`
787

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

791
            `basilisp test -- -h`
792

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

796
            `basilisp test -k vector -p other_dir`
797

798
        Returns the PyTest exit code as the exit code."""
799
    ),
800
    handler=test,
801
    allows_extra=True,
802
)
803
def _add_test_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
804
    _add_compiler_arg_group(parser)
1✔
805
    _add_import_arg_group(parser)
1✔
806
    _add_runtime_arg_group(parser)
1✔
807
    _add_debug_arg_group(parser)
1✔
808

809

810
def version(_, __) -> None:
1✔
811
    v = importlib.metadata.version("basilisp")
1✔
812
    print(f"Basilisp {v}")
1✔
813

814

815
@_subcommand("version", help="print the version of Basilisp", handler=version)
1✔
816
def _add_version_subcommand(_: argparse.ArgumentParser) -> None:
1✔
817
    pass
1✔
818

819

820
def run_script():
1✔
821
    """Entrypoint to run the Basilisp script named by `sys.argv[1]` as by the
822
    `basilisp run` subcommand.
823

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

827
    The current process is replaced as by `os.execvp`."""
828
    # os.exec* functions do not perform shell expansion, so we must do so manually.
829
    script_path = Path(sys.argv[1]).resolve()
×
830
    args = ["basilisp", "run", str(script_path)]
×
831
    # Collect arguments sent to the script and pass them onto `basilisp run`
832
    if rest := sys.argv[2:]:
×
833
        args.append("--")
×
834
        args.extend(rest)
×
835
    os.execvp("basilisp", args)  # nosec B606, B607
×
836

837

838
def invoke_cli(args: Optional[Sequence[str]] = None) -> None:
1✔
839
    """Entrypoint to run the Basilisp CLI."""
840
    parser = argparse.ArgumentParser(
1✔
841
        description="Basilisp is a Lisp dialect inspired by Clojure targeting Python 3."
842
    )
843

844
    subparsers = parser.add_subparsers(help="sub-commands")
1✔
845
    _add_bootstrap_subcommand(subparsers)
1✔
846
    _add_install_jar_subcommand(subparsers)
1✔
847
    _add_nrepl_server_subcommand(subparsers)
1✔
848
    _add_repl_subcommand(subparsers)
1✔
849
    _add_run_subcommand(subparsers)
1✔
850
    _add_test_subcommand(subparsers)
1✔
851
    _add_version_subcommand(subparsers)
1✔
852

853
    parsed_args, extra = parser.parse_known_args(args=args)
1✔
854
    allows_extra = getattr(parsed_args, "allows_extra", False)
1✔
855
    if extra and not allows_extra:
1✔
856
        parser.error(f"unrecognized arguments: {' '.join(extra)}")
×
857
    elif hasattr(parsed_args, "handler"):
1✔
858
        if allows_extra:
1✔
859
            parsed_args.handler(parser, parsed_args, extra)
×
860
        else:
861
            parsed_args.handler(parser, parsed_args)
1✔
862
    else:
863
        parser.print_help()
×
864

865

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

© 2026 Coveralls, Inc