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

basilisp-lang / basilisp / 10795001105

10 Sep 2024 02:47PM CUT coverage: 98.807% (-0.09%) from 98.892%
10795001105

Pull #1044

github

web-flow
Merge b864bfffa into ae617c7b3
Pull Request #1044: test runner for basilisp.test #980

1286 of 1294 branches covered (99.38%)

Branch coverage included in aggregate %.

8570 of 8681 relevant lines covered (98.72%)

0.99 hits per line

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

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

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

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

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

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

38

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

47

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

56

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

64

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

81

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

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

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

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

98

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

110

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

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

127
    return EnvVarSetterAction
1✔
128

129

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

254

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

268

269
def _add_debug_arg_group(parser: argparse.ArgumentParser) -> None:
1✔
270
    group = parser.add_argument_group("debug options")
1✔
271
    group.add_argument(
1✔
272
        "--disable-ns-cache",
273
        action=_set_envvar_action(
274
            "BASILISP_DO_NOT_CACHE_NAMESPACES", parent=argparse._StoreAction
275
        ),
276
        nargs="?",
277
        const=True,
278
        type=_to_bool,
279
        help=(
280
            "if true, disable attempting to load cached namespaces "
281
            "(env: BASILISP_DO_NOT_CACHE_NAMESPACES; default: false)"
282
        ),
283
    )
284

285

286
def _add_import_arg_group(parser: argparse.ArgumentParser) -> None:
1✔
287
    group = parser.add_argument_group(
1✔
288
        "path options",
289
        description=(
290
            "The path options below can be used to control how Basilisp (and Python) "
291
            "find your code."
292
        ),
293
    )
294
    group.add_argument(
1✔
295
        "--include-unsafe-path",
296
        action="store",
297
        nargs="?",
298
        const=True,
299
        default=os.getenv("BASILISP_INCLUDE_UNSAFE_PATH", "true"),
300
        type=_to_bool,
301
        help=(
302
            "if true, automatically prepend a potentially unsafe path to `sys.path`; "
303
            "setting `--include-unsafe-path=false` is the Basilisp equivalent to "
304
            "setting PYTHONSAFEPATH to a non-empty string for CPython's REPL "
305
            "(env: BASILISP_INCLUDE_UNSAFE_PATH; default: true)"
306
        ),
307
    )
308
    group.add_argument(
1✔
309
        "-p",
310
        "--include-path",
311
        action="append",
312
        help=(
313
            "path to prepend to `sys.path`; may be specified more than once to "
314
            "include multiple paths (env: PYTHONPATH)"
315
        ),
316
    )
317

318

319
def _add_runtime_arg_group(parser: argparse.ArgumentParser) -> None:
1✔
320
    group = parser.add_argument_group(
1✔
321
        "runtime arguments",
322
        description=(
323
            "The runtime arguments below affect reader and execution time features."
324
        ),
325
    )
326
    group.add_argument(
1✔
327
        "--data-readers-entry-points",
328
        action=_set_envvar_action(
329
            "BASILISP_USE_DATA_READERS_ENTRY_POINT", parent=argparse._StoreAction
330
        ),
331
        nargs="?",
332
        const=_to_bool(os.getenv("BASILISP_USE_DATA_READERS_ENTRY_POINT", "true")),
333
        type=_to_bool,
334
        help=(
335
            "if true, Load data readers from importlib entry points in the "
336
            '"basilisp_data_readers" group (env: '
337
            "BASILISP_USE_DATA_READERS_ENTRY_POINT; default: true)"
338
        ),
339
    )
340

341

342
Handler = Callable[[argparse.ArgumentParser, argparse.Namespace], None]
1✔
343

344

345
def _subcommand(
1✔
346
    subcommand: str,
347
    *,
348
    help: Optional[str] = None,  # pylint: disable=redefined-builtin
349
    description: Optional[str] = None,
350
    handler: Handler,
351
) -> Callable[
352
    [Callable[[argparse.ArgumentParser], None]],
353
    Callable[["argparse._SubParsersAction"], None],
354
]:
355
    def _wrap_add_subcommand(
1✔
356
        f: Callable[[argparse.ArgumentParser], None]
357
    ) -> Callable[["argparse._SubParsersAction"], None]:
358
        def _wrapped_subcommand(subparsers: "argparse._SubParsersAction"):
1✔
359
            parser = subparsers.add_parser(
1✔
360
                subcommand, help=help, description=description
361
            )
362
            parser.set_defaults(handler=handler)
1✔
363
            f(parser)
1✔
364

365
        return _wrapped_subcommand
1✔
366

367
    return _wrap_add_subcommand
1✔
368

369

370
def bootstrap_basilisp_installation(_, args: argparse.Namespace) -> None:
1✔
371
    if args.quiet:
1✔
372
        print_ = lambda v: v
1✔
373
    else:
374
        print_ = print
1✔
375

376
    if args.uninstall:
1✔
377
        if not (
1✔
378
            removed := basilisp.unbootstrap_python(site_packages=args.site_packages)
379
        ):
380
            print_("No Basilisp bootstrap files were found.")
1✔
381
        else:
382
            for file in removed:
1✔
383
                print_(f"Removed '{file}'")
1✔
384
    else:
385
        basilisp.bootstrap_python(site_packages=args.site_packages)
1✔
386
        print_(
1✔
387
            "Your Python installation has been bootstrapped! You can undo this at any "
388
            "time with with `basilisp bootstrap --uninstall`."
389
        )
390

391

392
@_subcommand(
1✔
393
    "bootstrap",
394
    help="bootstrap the Python installation to allow importing Basilisp namespaces",
395
    description=textwrap.dedent(
396
        """Bootstrap the Python installation to allow importing Basilisp namespaces"
397
        without requiring an additional bootstrapping step.
398

399
        Python installations are bootstrapped by installing a `basilispbootstrap.pth`
400
        file in your `site-packages` directory. Python installations execute `*.pth`
401
        files found at startup.
402

403
        Bootstrapping your Python installation in this way can help avoid needing to
404
        perform manual bootstrapping from Python code within your application.
405

406
        On the first startup, Basilisp will compile `basilisp.core` to byte code
407
        which could take up to 30 seconds in some cases depending on your system and
408
        which version of Python you are using. Subsequent startups should be
409
        considerably faster so long as you allow Basilisp to cache bytecode for
410
        namespaces."""
411
    ),
412
    handler=bootstrap_basilisp_installation,
413
)
414
def _add_bootstrap_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
415
    parser.add_argument(
1✔
416
        "--uninstall",
417
        action="store_true",
418
        help="if true, remove any `.pth` files installed by Basilisp in all site-packages directories",
419
    )
420
    parser.add_argument(
1✔
421
        "-q",
422
        "--quiet",
423
        action="store_true",
424
        help="if true, do not print out any",
425
    )
426
    # Allow specifying the "site-packages" directories via CLI argument for testing.
427
    # Not intended to be used by end users.
428
    parser.add_argument(
1✔
429
        "--site-packages",
430
        action="append",
431
        help=argparse.SUPPRESS,
432
    )
433

434

435
def nrepl_server(
1✔
436
    _,
437
    args: argparse.Namespace,
438
) -> None:
439
    basilisp.init(_compiler_opts(args))
1✔
440
    init_path(args)
1✔
441
    nrepl_server_mod = importlib.import_module(munge(NREPL_SERVER_NS))
1✔
442
    nrepl_server_mod.start_server__BANG__(
1✔
443
        lmap.map(
444
            {
445
                kw.keyword("host"): args.host,
446
                kw.keyword("port"): args.port,
447
                kw.keyword("nrepl-port-file"): args.port_filepath,
448
            }
449
        )
450
    )
451

452

453
@_subcommand(
1✔
454
    "nrepl-server",
455
    help="start the nREPL server",
456
    description="Start the nREPL server.",
457
    handler=nrepl_server,
458
)
459
def _add_nrepl_server_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
460
    parser.add_argument(
1✔
461
        "--host",
462
        default="127.0.0.1",
463
        help="the interface address to bind to, defaults to 127.0.0.1.",
464
    )
465
    parser.add_argument(
1✔
466
        "--port",
467
        default=0,
468
        type=int,
469
        help="the port to connect to, defaults to 0 (random available port).",
470
    )
471
    parser.add_argument(
1✔
472
        "--port-filepath",
473
        default=".nrepl-port",
474
        help='the file path where the server port number is output to, defaults to ".nrepl-port".',
475
    )
476
    _add_compiler_arg_group(parser)
1✔
477
    _add_import_arg_group(parser)
1✔
478
    _add_runtime_arg_group(parser)
1✔
479
    _add_debug_arg_group(parser)
1✔
480

481

482
def repl(
1✔
483
    _,
484
    args: argparse.Namespace,
485
) -> None:
486
    opts = _compiler_opts(args)
1✔
487
    basilisp.init(opts)
1✔
488
    init_path(args)
1✔
489
    ctx = compiler.CompilerContext(filename=REPL_INPUT_FILE_PATH, opts=opts)
1✔
490
    prompter = get_prompter()
1✔
491
    eof = object()
1✔
492

493
    # Bind user-settable dynamic Vars to their existing value to allow users to
494
    # conveniently (set! *var* val) at the REPL without needing `binding`.
495
    with runtime.bindings(
1✔
496
        {
497
            var: var.value
498
            for var in map(
499
                lambda name: runtime.Var.find_safe(
500
                    sym.symbol(name, ns=runtime.CORE_NS)
501
                ),
502
                [
503
                    "*e",
504
                    "*1",
505
                    "*2",
506
                    "*3",
507
                    "*assert*",
508
                    "*data-readers*",
509
                    "*resolver*",
510
                    runtime.PRINT_DUP_VAR_NAME,
511
                    runtime.PRINT_LEVEL_VAR_NAME,
512
                    runtime.PRINT_READABLY_VAR_NAME,
513
                    runtime.PRINT_LEVEL_VAR_NAME,
514
                    runtime.PRINT_META_VAR_NAME,
515
                    runtime.PRINT_NAMESPACE_MAPS_VAR_NAME,
516
                ],
517
            )
518
        }
519
    ):
520
        repl_module = bootstrap_repl(ctx, args.default_ns)
1✔
521
        ns_var = runtime.set_current_ns(args.default_ns)
1✔
522

523
        while True:
1✔
524
            ns: runtime.Namespace = ns_var.value
1✔
525
            try:
1✔
526
                lsrc = prompter.prompt(f"{ns.name}=> ")
1✔
527
            except EOFError:
1✔
528
                break
1✔
529
            except KeyboardInterrupt:  # pragma: no cover
530
                print("")
531
                continue
532

533
            if len(lsrc) == 0:
1✔
534
                continue
1✔
535

536
            try:
1✔
537
                result = eval_str(lsrc, ctx, ns, eof)
1✔
538
                if result is eof:  # pragma: no cover
539
                    continue
540
                prompter.print(runtime.lrepr(result))
1✔
541
                repl_module.mark_repl_result(result)
1✔
542
            except reader.SyntaxError as e:
1✔
543
                print_exception(e, reader.SyntaxError, e.__traceback__)
1✔
544
                repl_module.mark_exception(e)
1✔
545
                continue
1✔
546
            except compiler.CompilerException as e:
1✔
547
                print_exception(e, compiler.CompilerException, e.__traceback__)
1✔
548
                repl_module.mark_exception(e)
1✔
549
                continue
1✔
550
            except Exception as e:  # pylint: disable=broad-exception-caught
1✔
551
                print_exception(e, Exception, e.__traceback__)
1✔
552
                repl_module.mark_exception(e)
1✔
553
                continue
1✔
554

555

556
@_subcommand(
1✔
557
    "repl",
558
    help="start the Basilisp REPL",
559
    description="Start a Basilisp REPL.",
560
    handler=repl,
561
)
562
def _add_repl_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
563
    parser.add_argument(
1✔
564
        "--default-ns",
565
        default=runtime.REPL_DEFAULT_NS,
566
        help="default namespace to use for the REPL",
567
    )
568
    _add_compiler_arg_group(parser)
1✔
569
    _add_import_arg_group(parser)
1✔
570
    _add_runtime_arg_group(parser)
1✔
571
    _add_debug_arg_group(parser)
1✔
572

573

574
def run(
1✔
575
    parser: argparse.ArgumentParser,
576
    args: argparse.Namespace,
577
) -> None:
578
    target = args.file_or_ns_or_code
1✔
579
    if args.load_namespace:
1✔
580
        if args.in_ns is not None:
1✔
581
            parser.error(
1✔
582
                "argument --in-ns: not allowed with argument -n/--load-namespace"
583
            )
584
        in_ns = runtime.REPL_DEFAULT_NS
1✔
585
    else:
586
        in_ns = target if args.in_ns is not None else runtime.REPL_DEFAULT_NS
1✔
587

588
    opts = _compiler_opts(args)
1✔
589
    basilisp.init(opts)
1✔
590
    ctx = compiler.CompilerContext(
1✔
591
        filename=(
592
            CLI_INPUT_FILE_PATH
593
            if args.code
594
            else (STDIN_INPUT_FILE_PATH if target == STDIN_FILE_NAME else target)
595
        ),
596
        opts=opts,
597
    )
598
    eof = object()
1✔
599

600
    core_ns = runtime.Namespace.get(runtime.CORE_NS_SYM)
1✔
601
    assert core_ns is not None
1✔
602

603
    with runtime.ns_bindings(in_ns) as ns:
1✔
604
        ns.refer_all(core_ns)
1✔
605

606
        if args.args:
1✔
607
            cli_args_var = core_ns.find(sym.symbol(runtime.COMMAND_LINE_ARGS_VAR_NAME))
1✔
608
            assert cli_args_var is not None
1✔
609
            cli_args_var.bind_root(vec.vector(args.args))
1✔
610

611
        if args.code:
1✔
612
            init_path(args)
1✔
613
            eval_str(target, ctx, ns, eof)
1✔
614
        elif args.load_namespace:
1✔
615
            # Set the requested namespace as the *main-ns*
616
            main_ns_var = core_ns.find(sym.symbol(runtime.MAIN_NS_VAR_NAME))
1✔
617
            assert main_ns_var is not None
1✔
618
            main_ns_var.bind_root(sym.symbol(target))
1✔
619

620
            init_path(args)
1✔
621
            importlib.import_module(munge(target))
1✔
622
        elif target == STDIN_FILE_NAME:
1✔
623
            init_path(args)
1✔
624
            eval_stream(io.TextIOWrapper(sys.stdin.buffer, encoding="utf-8"), ctx, ns)
1✔
625
        else:
626
            init_path(args, unsafe_path=str(pathlib.Path(target).resolve().parent))
1✔
627
            eval_file(target, ctx, ns)
1✔
628

629

630
@_subcommand(
1✔
631
    "run",
632
    help="run a Basilisp script or code or namespace",
633
    description=textwrap.dedent(
634
        """Run a Basilisp script or a line of code or load a Basilisp namespace.
635

636
        If `-c` is provided, execute the line of code as given. If `-n` is given,
637
        interpret `file_or_ns_or_code` as a fully qualified Basilisp namespace
638
        relative to `sys.path`. Otherwise, execute the file as a script relative to
639
        the current working directory.
640

641
        `*main-ns*` will be set to the value provided for `-n`. In all other cases,
642
        it will be `nil`."""
643
    ),
644
    handler=run,
645
)
646
def _add_run_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
647
    parser.add_argument(
1✔
648
        "file_or_ns_or_code",
649
        help=(
650
            "file path to a Basilisp file, a string of Basilisp code, or a fully "
651
            "qualified Basilisp namespace name"
652
        ),
653
    )
654

655
    grp = parser.add_mutually_exclusive_group()
1✔
656
    grp.add_argument(
1✔
657
        "-c",
658
        "--code",
659
        action="store_true",
660
        help="if provided, treat argument as a string of code",
661
    )
662
    grp.add_argument(
1✔
663
        "-n",
664
        "--load-namespace",
665
        action="store_true",
666
        help="if provided, treat argument as the name of a namespace",
667
    )
668

669
    parser.add_argument(
1✔
670
        "--in-ns",
671
        help="namespace to use for the code (default: basilisp.user); ignored when `-n` is used",
672
    )
673
    parser.add_argument(
1✔
674
        "args",
675
        nargs=argparse.REMAINDER,
676
        help="command line args made accessible to the script as basilisp.core/*command-line-args*",
677
    )
678
    _add_compiler_arg_group(parser)
1✔
679
    _add_import_arg_group(parser)
1✔
680
    _add_runtime_arg_group(parser)
1✔
681
    _add_debug_arg_group(parser)
1✔
682

683

684
def test(
685
    parser: argparse.ArgumentParser, args: argparse.Namespace
686
) -> None:  # pragma: no cover
687
    basilisp.init(_compiler_opts(args))
688
    try:
689
        import pytest
690
    except (ImportError, ModuleNotFoundError):
691
        parser.error(
692
            "Cannot run tests without dependency PyTest. Please install PyTest and try again.",
693
        )
694
    else:
695
        pytest.main(args=list(args.args))
696

697

698
@_subcommand(
1✔
699
    "test",
700
    help="run tests in a Basilisp project",
701
    description="Run tests in a Basilisp project.",
702
    handler=test,
703
)
704
def _add_test_subcommand(parser: argparse.ArgumentParser) -> None:
1✔
705
    parser.add_argument("args", nargs=-1)
1✔
706
    _add_compiler_arg_group(parser)
1✔
707
    _add_runtime_arg_group(parser)
1✔
708
    _add_debug_arg_group(parser)
1✔
709

710

711
def version(_, __) -> None:
1✔
712
    v = importlib.metadata.version("basilisp")
1✔
713
    print(f"Basilisp {v}")
1✔
714

715

716
@_subcommand("version", help="print the version of Basilisp", handler=version)
1✔
717
def _add_version_subcommand(_: argparse.ArgumentParser) -> None:
1✔
718
    pass
1✔
719

720

721
def run_script():
1✔
722
    """Entrypoint to run the Basilisp script named by `sys.argv[1]` as by the
723
    `basilisp run` subcommand.
724

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

728
    The current process is replaced as by `os.execlp`."""
729
    # os.exec* functions do not perform shell expansion, so we must do so manually.
730
    script_path = Path(sys.argv[1]).resolve()
×
731
    args = ["basilisp", "run", str(script_path)]
×
732
    # Collect arguments sent to the script and pass them onto `basilisp run`
733
    if rest := sys.argv[2:]:
×
734
        args.append("--")
×
735
        args.extend(rest)
×
736
    os.execvp("basilisp", args)
×
737

738

739
def invoke_cli(args: Optional[Sequence[str]] = None) -> None:
1✔
740
    """Entrypoint to run the Basilisp CLI."""
741
    parser = argparse.ArgumentParser(
1✔
742
        description="Basilisp is a Lisp dialect inspired by Clojure targeting Python 3."
743
    )
744

745
    subparsers = parser.add_subparsers(help="sub-commands")
1✔
746
    _add_bootstrap_subcommand(subparsers)
1✔
747
    _add_nrepl_server_subcommand(subparsers)
1✔
748
    _add_repl_subcommand(subparsers)
1✔
749
    _add_run_subcommand(subparsers)
1✔
750
    _add_test_subcommand(subparsers)
1✔
751
    _add_version_subcommand(subparsers)
1✔
752

753
    parsed_args = parser.parse_args(args=args)
1✔
754
    if hasattr(parsed_args, "handler"):
1✔
755
        parsed_args.handler(parser, parsed_args)
1✔
756
    else:
757
        parser.print_help()
×
758

759

760
if __name__ == "__main__":
761
    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