Skip to content

Commit 03f1679

Browse files
authored
Allow adding additional sys.path entries from the CLI (#1036)
Fixes #1027
1 parent 1cabd6d commit 03f1679

File tree

6 files changed

+417
-4
lines changed

6 files changed

+417
-4
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Added
9+
* Added the `-p`/`--include-path` CLI command to prepend entries to the `sys.path` as an alternative to `PYTHONPATH` (#1027)
10+
* Added an empty entry to `sys.path` for all CLI entrypoints (`basilisp run`, `basilisp repl`, etc.) (#1027)
11+
812
### Changed
913
* The compiler will no longer require `Var` indirection for top-level `do` forms unless those forms specify `^:use-var-indirection` metadata (which currently is only used in the `ns` macro) (#1034)
1014
* nREPL server no longer sends ANSI color escape sequences in exception messages to clients (#1039)
1115

1216
### Fixed
1317
* Fix a bug where the compiler would always generate inline function definitions even if the `inline-functions` compiler option is disabled (#1023)
14-
* Fix a bug where `defrecord`/`deftype` constructors could not be used in the type's methods. (#1025)
18+
* Fix a bug where `defrecord`/`deftype` constructors could not be used in the type's methods (#1025)
1519
* Fix a bug where `keys` and `vals` would fail for records (#1030)
1620
* Fix a bug where operations on records created by `defrecord` failed for fields whose Python-safe names were mangled by the Python compiler (#1029)
1721
* Fix incorrect line numbers for compiler exceptions in nREPL when evaluating forms in loaded files (#1037)

docs/cli.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,28 @@ Basilisp exposes all of it's available configuration options as CLI flags and en
1414
All Basilisp CLI subcommands which include configuration note the available configuration options when the ``-h`` and ``--help`` flags are given.
1515
Generally the Basilisp CLI configuration options are simple passthroughs that correspond to :ref:`configuration options for the compiler <compiler_configuration>`.
1616

17+
.. _cli_path_configuration:
18+
19+
``PYTHONPATH`` Configuration
20+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
21+
22+
Basilisp uses the ``PYTHONPATH`` environment variable and :external:py:data:`sys.path` to determine where to look for Basilisp code when :ref:`requiring namespaces <namespace_requires>`.
23+
Additional values may be set using the ``-p`` (or ``--include-path``) CLI flags.
24+
Depending on how Basilisp is invoked from the CLI, an additional entry will automatically be added unless explicitly disabled using ``--include-unsafe-path=false``:
25+
26+
* An empty string (which implies the current working directory) will be prepended to the ``sys.path`` in the following cases:
27+
28+
* Starting a REPL
29+
* Running a string of code directly (using ``run -c``)
30+
* Running code directly from ``stdin`` (using ``run -``)
31+
* Running a namespace directly (using ``run -n``)
32+
33+
* When running a script directly (as by ``run /path/to/script.lpy``), the parent directory of the script will be prepended to ``sys.path``
34+
35+
.. seealso::
36+
37+
:ref:`pythonpath_configuration`
38+
1739
.. _start_a_repl_session:
1840

1941
Start a REPL Session

docs/reader.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,9 +474,10 @@ Custom Data Readers
474474

475475
When Basilisp starts it can load data readers from multiple sources.
476476

477-
It will search in :external:py:attr:`sys.path` for files named ``data_readers.lpy`` or else ``data_readers.cljc``; each which must contain a mapping of qualified symbol tags to qualified symbols of function vars.
477+
It will search in :external:py:data:`sys.path` for files named ``data_readers.lpy`` or else ``data_readers.cljc``; each which must contain a mapping of qualified symbol tags to qualified symbols of function vars.
478478

479479
.. code-block:: clojure
480+
480481
{my/tag my.namespace/tag-handler}
481482
482483
It will also search for any :external:py:class:`importlib.metadata.EntryPoint` in the group ``basilisp_data_readers`` group.

docs/runtime.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ See the documentation for :lpy:fn:`require` for more details.
101101

102102
:lpy:fn:`ns-aliases`, :lpy:fn:`ns-interns`, :lpy:fn:`ns-map`, :lpy:fn:`ns-publics`, :lpy:fn:`ns-refers`, :lpy:fn:`ns-unalias`, :lpy:fn:`ns-unmap`, :lpy:fn:`refer`, :lpy:fn:`require`, :lpy:fn:`use`
103103

104+
.. _pythonpath_configuration:
105+
106+
``PYTHONPATH``, ``sys.path``, and Finding Basilisp Namespaces
107+
#############################################################
108+
109+
Basilisp uses the ``PYTHONPATH`` environment variable and :external:py:data:`sys.path` to determine where to look for Basilisp code when requiring namespaces.
110+
This is roughly analogous to the Java classpath in Clojure.
111+
These values may be set manually, but are more often configured by some project management tool such as Poetry or defined in your Python virtualenv.
112+
These values may also be set via :ref:`cli` arguments.
113+
104114
.. _vars:
105115

106116
Vars

src/basilisp/cli.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import importlib.metadata
33
import io
44
import os
5+
import pathlib
56
import sys
67
import textwrap
78
import types
@@ -78,6 +79,23 @@ def bootstrap_repl(ctx: compiler.CompilerContext, which_ns: str) -> types.Module
7879
return importlib.import_module(REPL_NS)
7980

8081

82+
def init_path(args: argparse.Namespace, unsafe_path: str = "") -> None:
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:
87+
if path in sys.path:
88+
return
89+
sys.path.insert(0, path)
90+
91+
for pth in args.include_path or []:
92+
p = pathlib.Path(pth).resolve()
93+
prepend_once(str(p))
94+
95+
if args.include_unsafe_path:
96+
prepend_once(unsafe_path)
97+
98+
8199
def _to_bool(v: Optional[str]) -> Optional[bool]:
82100
"""Coerce a string argument to a boolean value, if possible."""
83101
if v is None:
@@ -265,6 +283,39 @@ def _add_debug_arg_group(parser: argparse.ArgumentParser) -> None:
265283
)
266284

267285

286+
def _add_import_arg_group(parser: argparse.ArgumentParser) -> None:
287+
group = parser.add_argument_group(
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(
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(
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+
268319
def _add_runtime_arg_group(parser: argparse.ArgumentParser) -> None:
269320
group = parser.add_argument_group(
270321
"runtime arguments",
@@ -281,8 +332,8 @@ def _add_runtime_arg_group(parser: argparse.ArgumentParser) -> None:
281332
const=_to_bool(os.getenv("BASILISP_USE_DATA_READERS_ENTRY_POINT", "true")),
282333
type=_to_bool,
283334
help=(
284-
"If true, Load data readers from importlib entry points in the "
285-
'"basilisp_data_readers" group. (env: '
335+
"if true, Load data readers from importlib entry points in the "
336+
'"basilisp_data_readers" group (env: '
286337
"BASILISP_USE_DATA_READERS_ENTRY_POINT; default: true)"
287338
),
288339
)
@@ -386,6 +437,7 @@ def nrepl_server(
386437
args: argparse.Namespace,
387438
) -> None:
388439
basilisp.init(_compiler_opts(args))
440+
init_path(args)
389441
nrepl_server_mod = importlib.import_module(munge(NREPL_SERVER_NS))
390442
nrepl_server_mod.start_server__BANG__(
391443
lmap.map(
@@ -422,6 +474,7 @@ def _add_nrepl_server_subcommand(parser: argparse.ArgumentParser) -> None:
422474
help='the file path where the server port number is output to, defaults to ".nrepl-port".',
423475
)
424476
_add_compiler_arg_group(parser)
477+
_add_import_arg_group(parser)
425478
_add_runtime_arg_group(parser)
426479
_add_debug_arg_group(parser)
427480

@@ -432,6 +485,7 @@ def repl(
432485
) -> None:
433486
opts = _compiler_opts(args)
434487
basilisp.init(opts)
488+
init_path(args)
435489
ctx = compiler.CompilerContext(filename=REPL_INPUT_FILE_PATH, opts=opts)
436490
prompter = get_prompter()
437491
eof = object()
@@ -512,6 +566,7 @@ def _add_repl_subcommand(parser: argparse.ArgumentParser) -> None:
512566
help="default namespace to use for the REPL",
513567
)
514568
_add_compiler_arg_group(parser)
569+
_add_import_arg_group(parser)
515570
_add_runtime_arg_group(parser)
516571
_add_debug_arg_group(parser)
517572

@@ -554,17 +609,21 @@ def run(
554609
cli_args_var.bind_root(vec.vector(args.args))
555610

556611
if args.code:
612+
init_path(args)
557613
eval_str(target, ctx, ns, eof)
558614
elif args.load_namespace:
559615
# Set the requested namespace as the *main-ns*
560616
main_ns_var = core_ns.find(sym.symbol(runtime.MAIN_NS_VAR_NAME))
561617
assert main_ns_var is not None
562618
main_ns_var.bind_root(sym.symbol(target))
563619

620+
init_path(args)
564621
importlib.import_module(munge(target))
565622
elif target == STDIN_FILE_NAME:
623+
init_path(args)
566624
eval_stream(io.TextIOWrapper(sys.stdin.buffer, encoding="utf-8"), ctx, ns)
567625
else:
626+
init_path(args, unsafe_path=str(pathlib.Path(target).resolve().parent))
568627
eval_file(target, ctx, ns)
569628

570629

@@ -617,6 +676,7 @@ def _add_run_subcommand(parser: argparse.ArgumentParser) -> None:
617676
help="command line args made accessible to the script as basilisp.core/*command-line-args*",
618677
)
619678
_add_compiler_arg_group(parser)
679+
_add_import_arg_group(parser)
620680
_add_runtime_arg_group(parser)
621681
_add_debug_arg_group(parser)
622682

0 commit comments

Comments
 (0)