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

basilisp-lang / basilisp / 10692400497

03 Sep 2024 11:46PM CUT coverage: 98.898%. First build
10692400497

Pull #1031

github

web-flow
Merge f373f79c9 into 7f1b75ad7
Pull Request #1031: Fix a bug with using fields with special characters in records

1870 of 1877 branches covered (99.63%)

Branch coverage included in aggregate %.

8542 of 8651 relevant lines covered (98.74%)

0.99 hits per line

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

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

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

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

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

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

37

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

46

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

55

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

63

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

80

81
def _to_bool(v: Optional[str]) -> Optional[bool]:
1✔
82
    """Coerce a string argument to a boolean value, if possible."""
83
    if v is None:
1✔
84
        return v
×
85
    elif v.lower() in BOOL_TRUE:
1✔
86
        return True
1✔
87
    elif v.lower() in BOOL_FALSE:
1✔
88
        return False
1✔
89
    else:
90
        raise argparse.ArgumentTypeError("Unable to coerce flag value to boolean.")
1✔
91

92

93
def _set_envvar_action(
1✔
94
    var: str, parent: Type[argparse.Action] = argparse.Action
95
) -> Type[argparse.Action]:
96
    """Return an argparse.Action instance (deriving from `parent`) that sets the value
97
    as the default value of the environment variable `var`."""
98

99
    class EnvVarSetterAction(parent):  # type: ignore
1✔
100
        def __call__(  # pylint: disable=signature-differs
1✔
101
            self,
102
            parser: argparse.ArgumentParser,
103
            namespace: argparse.Namespace,
104
            values: Any,
105
            option_string: str,
106
        ):
107
            os.environ.setdefault(var, str(values))
1✔
108

109
    return EnvVarSetterAction
1✔
110

111

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

236

237
def _compiler_opts(args: argparse.Namespace) -> CompilerOpts:
1✔
238
    return compiler.compiler_opts(
1✔
239
        generate_auto_inlines=args.generate_auto_inlines,
240
        inline_functions=args.inline_functions,
241
        warn_on_arity_mismatch=args.warn_on_arity_mismatch,
242
        warn_on_shadowed_name=args.warn_on_shadowed_name,
243
        warn_on_shadowed_var=args.warn_on_shadowed_var,
244
        warn_on_non_dynamic_set=args.warn_on_non_dynamic_set,
245
        warn_on_unused_names=args.warn_on_unused_names,
246
        use_var_indirection=args.use_var_indirection,
247
        warn_on_var_indirection=args.warn_on_var_indirection,
248
    )
249

250

251
def _add_debug_arg_group(parser: argparse.ArgumentParser) -> None:
1✔
252
    group = parser.add_argument_group("debug options")
1✔
253
    group.add_argument(
1✔
254
        "--disable-ns-cache",
255
        action=_set_envvar_action(
256
            "BASILISP_DO_NOT_CACHE_NAMESPACES", parent=argparse._StoreAction
257
        ),
258
        nargs="?",
259
        const=True,
260
        type=_to_bool,
261
        help=(
262
            "if true, disable attempting to load cached namespaces "
263
            "(env: BASILISP_DO_NOT_CACHE_NAMESPACES; default: false)"
264
        ),
265
    )
266

267

268
def _add_runtime_arg_group(parser: argparse.ArgumentParser) -> None:
1✔
269
    group = parser.add_argument_group(
1✔
270
        "runtime arguments",
271
        description=(
272
            "The runtime arguments below affect reader and execution time features."
273
        ),
274
    )
275
    group.add_argument(
1✔
276
        "--data-readers-entry-points",
277
        action=_set_envvar_action(
278
            "BASILISP_USE_DATA_READERS_ENTRY_POINT", parent=argparse._StoreAction
279
        ),
280
        nargs="?",
281
        const=_to_bool(os.getenv("BASILISP_USE_DATA_READERS_ENTRY_POINT", "true")),
282
        type=_to_bool,
283
        help=(
284
            "If true, Load data readers from importlib entry points in the "
285
            '"basilisp_data_readers" group. (env: '
286
            "BASILISP_USE_DATA_READERS_ENTRY_POINT; default: true)"
287
        ),
288
    )
289

290

291
Handler = Callable[[argparse.ArgumentParser, argparse.Namespace], None]
1✔
292

293

294
def _subcommand(
1✔
295
    subcommand: str,
296
    *,
297
    help: Optional[str] = None,  # pylint: disable=redefined-builtin
298
    description: Optional[str] = None,
299
    handler: Handler,
300
) -> Callable[
301
    [Callable[[argparse.ArgumentParser], None]],
302
    Callable[["argparse._SubParsersAction"], None],
303
]:
304
    def _wrap_add_subcommand(
1✔
305
        f: Callable[[argparse.ArgumentParser], None]
306
    ) -> Callable[["argparse._SubParsersAction"], None]:
307
        def _wrapped_subcommand(subparsers: "argparse._SubParsersAction"):
1✔
308
            parser = subparsers.add_parser(
1✔
309
                subcommand, help=help, description=description
310
            )
311
            parser.set_defaults(handler=handler)
1✔
312
            f(parser)
1✔
313

314
        return _wrapped_subcommand
1✔
315

316
    return _wrap_add_subcommand
1✔
317

318

319
def bootstrap_basilisp_installation(_, args: argparse.Namespace) -> None:
1✔
320
    if args.quiet:
1✔
321
        print_ = lambda v: v
1✔
322
    else:
323
        print_ = print
1✔
324

325
    if args.uninstall:
1✔
326
        if not (
1✔
327
            removed := basilisp.unbootstrap_python(site_packages=args.site_packages)
328
        ):
329
            print_("No Basilisp bootstrap files were found.")
1✔
330
        else:
331
            for file in removed:
1✔
332
                print_(f"Removed '{file}'")
1✔
333
    else:
334
        basilisp.bootstrap_python(site_packages=args.site_packages)
1✔
335
        print_(
1✔
336
            "Your Python installation has been bootstrapped! You can undo this at any "
337
            "time with with `basilisp bootstrap --uninstall`."
338
        )
339

340

341
@_subcommand(
1✔
342
    "bootstrap",
343
    help="bootstrap the Python installation to allow importing Basilisp namespaces",
344
    description=textwrap.dedent(
345
        """Bootstrap the Python installation to allow importing Basilisp namespaces"
346
        without requiring an additional bootstrapping step.
347

348
        Python installations are bootstrapped by installing a `basilispbootstrap.pth`
349
        file in your `site-packages` directory. Python installations execute `*.pth`
350
        files found at startup.
351

352
        Bootstrapping your Python installation in this way can help avoid needing to
353
        perform manual bootstrapping from Python code within your application.
354

355
        On the first startup, Basilisp will compile `basilisp.core` to byte code
356
        which could take up to 30 seconds in some cases depending on your system and
357
        which version of Python you are using. Subsequent startups should be
358
        considerably faster so long as you allow Basilisp to cache bytecode for
359
        namespaces."""
360
    ),
361
    handler=bootstrap_basilisp_installation,
362
)
363
def _add_bootstrap_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
364
    parser.add_argument(
1✔
365
        "--uninstall",
366
        action="store_true",
367
        help="if true, remove any `.pth` files installed by Basilisp in all site-packages directories",
368
    )
369
    parser.add_argument(
1✔
370
        "-q",
371
        "--quiet",
372
        action="store_true",
373
        help="if true, do not print out any",
374
    )
375
    # Allow specifying the "site-packages" directories via CLI argument for testing.
376
    # Not intended to be used by end users.
377
    parser.add_argument(
1✔
378
        "--site-packages",
379
        action="append",
380
        help=argparse.SUPPRESS,
381
    )
382

383

384
def nrepl_server(
1✔
385
    _,
386
    args: argparse.Namespace,
387
) -> None:
388
    basilisp.init(_compiler_opts(args))
1✔
389
    nrepl_server_mod = importlib.import_module(munge(NREPL_SERVER_NS))
1✔
390
    nrepl_server_mod.start_server__BANG__(
1✔
391
        lmap.map(
392
            {
393
                kw.keyword("host"): args.host,
394
                kw.keyword("port"): args.port,
395
                kw.keyword("nrepl-port-file"): args.port_filepath,
396
            }
397
        )
398
    )
399

400

401
@_subcommand(
1✔
402
    "nrepl-server",
403
    help="start the nREPL server",
404
    description="Start the nREPL server.",
405
    handler=nrepl_server,
406
)
407
def _add_nrepl_server_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
408
    parser.add_argument(
1✔
409
        "--host",
410
        default="127.0.0.1",
411
        help="the interface address to bind to, defaults to 127.0.0.1.",
412
    )
413
    parser.add_argument(
1✔
414
        "--port",
415
        default=0,
416
        type=int,
417
        help="the port to connect to, defaults to 0 (random available port).",
418
    )
419
    parser.add_argument(
1✔
420
        "--port-filepath",
421
        default=".nrepl-port",
422
        help='the file path where the server port number is output to, defaults to ".nrepl-port".',
423
    )
424
    _add_compiler_arg_group(parser)
1✔
425
    _add_runtime_arg_group(parser)
1✔
426
    _add_debug_arg_group(parser)
1✔
427

428

429
def repl(
1✔
430
    _,
431
    args: argparse.Namespace,
432
) -> None:
433
    opts = _compiler_opts(args)
1✔
434
    basilisp.init(opts)
1✔
435
    ctx = compiler.CompilerContext(filename=REPL_INPUT_FILE_PATH, opts=opts)
1✔
436
    prompter = get_prompter()
1✔
437
    eof = object()
1✔
438

439
    # Bind user-settable dynamic Vars to their existing value to allow users to
440
    # conveniently (set! *var* val) at the REPL without needing `binding`.
441
    with runtime.bindings(
1✔
442
        {
443
            var: var.value
444
            for var in map(
445
                lambda name: runtime.Var.find_safe(
446
                    sym.symbol(name, ns=runtime.CORE_NS)
447
                ),
448
                [
449
                    "*e",
450
                    "*1",
451
                    "*2",
452
                    "*3",
453
                    "*assert*",
454
                    "*data-readers*",
455
                    "*resolver*",
456
                    runtime.PRINT_DUP_VAR_NAME,
457
                    runtime.PRINT_LEVEL_VAR_NAME,
458
                    runtime.PRINT_READABLY_VAR_NAME,
459
                    runtime.PRINT_LEVEL_VAR_NAME,
460
                    runtime.PRINT_META_VAR_NAME,
461
                    runtime.PRINT_NAMESPACE_MAPS_VAR_NAME,
462
                ],
463
            )
464
        }
465
    ):
466
        repl_module = bootstrap_repl(ctx, args.default_ns)
1✔
467
        ns_var = runtime.set_current_ns(args.default_ns)
1✔
468

469
        while True:
1✔
470
            ns: runtime.Namespace = ns_var.value
1✔
471
            try:
1✔
472
                lsrc = prompter.prompt(f"{ns.name}=> ")
1✔
473
            except EOFError:
1✔
474
                break
1✔
475
            except KeyboardInterrupt:  # pragma: no cover
476
                print("")
477
                continue
478

479
            if len(lsrc) == 0:
1✔
480
                continue
1✔
481

482
            try:
1✔
483
                result = eval_str(lsrc, ctx, ns, eof)
1✔
484
                if result is eof:  # pragma: no cover
485
                    continue
486
                prompter.print(runtime.lrepr(result))
1✔
487
                repl_module.mark_repl_result(result)
1✔
488
            except reader.SyntaxError as e:
1✔
489
                print_exception(e, reader.SyntaxError, e.__traceback__)
1✔
490
                repl_module.mark_exception(e)
1✔
491
                continue
1✔
492
            except compiler.CompilerException as e:
1✔
493
                print_exception(e, compiler.CompilerException, e.__traceback__)
1✔
494
                repl_module.mark_exception(e)
1✔
495
                continue
1✔
496
            except Exception as e:  # pylint: disable=broad-exception-caught
1✔
497
                print_exception(e, Exception, e.__traceback__)
1✔
498
                repl_module.mark_exception(e)
1✔
499
                continue
1✔
500

501

502
@_subcommand(
1✔
503
    "repl",
504
    help="start the Basilisp REPL",
505
    description="Start a Basilisp REPL.",
506
    handler=repl,
507
)
508
def _add_repl_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
509
    parser.add_argument(
1✔
510
        "--default-ns",
511
        default=runtime.REPL_DEFAULT_NS,
512
        help="default namespace to use for the REPL",
513
    )
514
    _add_compiler_arg_group(parser)
1✔
515
    _add_runtime_arg_group(parser)
1✔
516
    _add_debug_arg_group(parser)
1✔
517

518

519
def run(
1✔
520
    parser: argparse.ArgumentParser,
521
    args: argparse.Namespace,
522
) -> None:
523
    target = args.file_or_ns_or_code
1✔
524
    if args.load_namespace:
1✔
525
        if args.in_ns is not None:
1✔
526
            parser.error(
1✔
527
                "argument --in-ns: not allowed with argument -n/--load-namespace"
528
            )
529
        in_ns = runtime.REPL_DEFAULT_NS
1✔
530
    else:
531
        in_ns = target if args.in_ns is not None else runtime.REPL_DEFAULT_NS
1✔
532

533
    opts = _compiler_opts(args)
1✔
534
    basilisp.init(opts)
1✔
535
    ctx = compiler.CompilerContext(
1✔
536
        filename=(
537
            CLI_INPUT_FILE_PATH
538
            if args.code
539
            else (STDIN_INPUT_FILE_PATH if target == STDIN_FILE_NAME else target)
540
        ),
541
        opts=opts,
542
    )
543
    eof = object()
1✔
544

545
    core_ns = runtime.Namespace.get(runtime.CORE_NS_SYM)
1✔
546
    assert core_ns is not None
1✔
547

548
    with runtime.ns_bindings(in_ns) as ns:
1✔
549
        ns.refer_all(core_ns)
1✔
550

551
        if args.args:
1✔
552
            cli_args_var = core_ns.find(sym.symbol(runtime.COMMAND_LINE_ARGS_VAR_NAME))
1✔
553
            assert cli_args_var is not None
1✔
554
            cli_args_var.bind_root(vec.vector(args.args))
1✔
555

556
        if args.code:
1✔
557
            eval_str(target, ctx, ns, eof)
1✔
558
        elif args.load_namespace:
1✔
559
            # Set the requested namespace as the *main-ns*
560
            main_ns_var = core_ns.find(sym.symbol(runtime.MAIN_NS_VAR_NAME))
1✔
561
            assert main_ns_var is not None
1✔
562
            main_ns_var.bind_root(sym.symbol(target))
1✔
563

564
            importlib.import_module(munge(target))
1✔
565
        elif target == STDIN_FILE_NAME:
1✔
566
            eval_stream(io.TextIOWrapper(sys.stdin.buffer, encoding="utf-8"), ctx, ns)
1✔
567
        else:
568
            eval_file(target, ctx, ns)
1✔
569

570

571
@_subcommand(
1✔
572
    "run",
573
    help="run a Basilisp script or code or namespace",
574
    description=textwrap.dedent(
575
        """Run a Basilisp script or a line of code or load a Basilisp namespace.
576

577
        If `-c` is provided, execute the line of code as given. If `-n` is given,
578
        interpret `file_or_ns_or_code` as a fully qualified Basilisp namespace
579
        relative to `sys.path`. Otherwise, execute the file as a script relative to
580
        the current working directory.
581

582
        `*main-ns*` will be set to the value provided for `-n`. In all other cases,
583
        it will be `nil`."""
584
    ),
585
    handler=run,
586
)
587
def _add_run_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
588
    parser.add_argument(
1✔
589
        "file_or_ns_or_code",
590
        help=(
591
            "file path to a Basilisp file, a string of Basilisp code, or a fully "
592
            "qualified Basilisp namespace name"
593
        ),
594
    )
595

596
    grp = parser.add_mutually_exclusive_group()
1✔
597
    grp.add_argument(
1✔
598
        "-c",
599
        "--code",
600
        action="store_true",
601
        help="if provided, treat argument as a string of code",
602
    )
603
    grp.add_argument(
1✔
604
        "-n",
605
        "--load-namespace",
606
        action="store_true",
607
        help="if provided, treat argument as the name of a namespace",
608
    )
609

610
    parser.add_argument(
1✔
611
        "--in-ns",
612
        help="namespace to use for the code (default: basilisp.user); ignored when `-n` is used",
613
    )
614
    parser.add_argument(
1✔
615
        "args",
616
        nargs=argparse.REMAINDER,
617
        help="command line args made accessible to the script as basilisp.core/*command-line-args*",
618
    )
619
    _add_compiler_arg_group(parser)
1✔
620
    _add_runtime_arg_group(parser)
1✔
621
    _add_debug_arg_group(parser)
1✔
622

623

624
def test(
625
    parser: argparse.ArgumentParser, args: argparse.Namespace
626
) -> None:  # pragma: no cover
627
    basilisp.init(_compiler_opts(args))
628
    try:
629
        import pytest
630
    except (ImportError, ModuleNotFoundError):
631
        parser.error(
632
            "Cannot run tests without dependency PyTest. Please install PyTest and try again.",
633
        )
634
    else:
635
        pytest.main(args=list(args.args))
636

637

638
@_subcommand(
1✔
639
    "test",
640
    help="run tests in a Basilisp project",
641
    description="Run tests in a Basilisp project.",
642
    handler=test,
643
)
644
def _add_test_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
645
    parser.add_argument("args", nargs=-1)
1✔
646
    _add_compiler_arg_group(parser)
1✔
647
    _add_runtime_arg_group(parser)
1✔
648
    _add_debug_arg_group(parser)
1✔
649

650

651
def version(_, __) -> None:
1✔
652
    v = importlib.metadata.version("basilisp")
1✔
653
    print(f"Basilisp {v}")
1✔
654

655

656
@_subcommand("version", help="print the version of Basilisp", handler=version)
1✔
657
def _add_version_subcommand(_: argparse.ArgumentParser) -> None:
1✔
658
    pass
1✔
659

660

661
def run_script():
1✔
662
    """Entrypoint to run the Basilisp script named by `sys.argv[1]` as by the
663
    `basilisp run` subcommand.
664

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

668
    The current process is replaced as by `os.execlp`."""
669
    # os.exec* functions do not perform shell expansion, so we must do so manually.
670
    script_path = Path(sys.argv[1]).resolve()
×
671
    args = ["basilisp", "run", str(script_path)]
×
672
    # Collect arguments sent to the script and pass them onto `basilisp run`
673
    if rest := sys.argv[2:]:
×
674
        args.append("--")
×
675
        args.extend(rest)
×
676
    os.execvp("basilisp", args)
×
677

678

679
def invoke_cli(args: Optional[Sequence[str]] = None) -> None:
1✔
680
    """Entrypoint to run the Basilisp CLI."""
681
    parser = argparse.ArgumentParser(
1✔
682
        description="Basilisp is a Lisp dialect inspired by Clojure targeting Python 3."
683
    )
684

685
    subparsers = parser.add_subparsers(help="sub-commands")
1✔
686
    _add_bootstrap_subcommand(subparsers)
1✔
687
    _add_nrepl_server_subcommand(subparsers)
1✔
688
    _add_repl_subcommand(subparsers)
1✔
689
    _add_run_subcommand(subparsers)
1✔
690
    _add_test_subcommand(subparsers)
1✔
691
    _add_version_subcommand(subparsers)
1✔
692

693
    parsed_args = parser.parse_args(args=args)
1✔
694
    if hasattr(parsed_args, "handler"):
1✔
695
        parsed_args.handler(parser, parsed_args)
1✔
696
    else:
697
        parser.print_help()
×
698

699

700
if __name__ == "__main__":
701
    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