Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow adding additional sys.path entries from the CLI #1036

Merged
merged 13 commits into from
Sep 7, 2024
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
* Added the `-p`/`--include-path` CLI command to prepend entries to the `sys.path` as an alternative to `PYTHONPATH` (#1027)
* Added an empty entry to `sys.path` for all CLI entrypoints (`basilisp run`, `basilisp repl`, etc.) (#1027)

### Changed
* 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)
* nREPL server no longer sends ANSI color escape sequences in exception messages to clients (#1039)

### Fixed
* Fix a bug where the compiler would always generate inline function definitions even if the `inline-functions` compiler option is disabled (#1023)
* Fix a bug where `defrecord`/`deftype` constructors could not be used in the type's methods. (#1025)
* Fix a bug where `defrecord`/`deftype` constructors could not be used in the type's methods (#1025)
* Fix a bug where `keys` and `vals` would fail for records (#1030)
* Fix a bug where operations on records created by `defrecord` failed for fields whose Python-safe names were mangled by the Python compiler (#1029)
* Fix incorrect line numbers for compiler exceptions in nREPL when evaluating forms in loaded files (#1037)
Expand Down
22 changes: 22 additions & 0 deletions docs/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,28 @@ Basilisp exposes all of it's available configuration options as CLI flags and en
All Basilisp CLI subcommands which include configuration note the available configuration options when the ``-h`` and ``--help`` flags are given.
Generally the Basilisp CLI configuration options are simple passthroughs that correspond to :ref:`configuration options for the compiler <compiler_configuration>`.

.. _cli_path_configuration:

``PYTHONPATH`` Configuration
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

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>`.
Additional values may be set using the ``-p`` (or ``--include-path``) CLI flags.
Depending on how Basilisp is invoked from the CLI, an additional entry will automatically be added unless explicitly disabled using ``--include-unsafe-path=false``:

* An empty string (which implies the current working directory) will be prepended to the ``sys.path`` in the following cases:

* Starting a REPL
* Running a string of code directly (using ``run -c``)
* Running code directly from ``stdin`` (using ``run -``)
* Running a namespace directly (using ``run -n``)

* When running a script directly (as by ``run /path/to/script.lpy``), the parent directory of the script will be prepended to ``sys.path``

.. seealso::

:ref:`pythonpath_configuration`

.. _start_a_repl_session:

Start a REPL Session
Expand Down
3 changes: 2 additions & 1 deletion docs/reader.rst
Original file line number Diff line number Diff line change
Expand Up @@ -474,9 +474,10 @@ Custom Data Readers

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

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.
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.

.. code-block:: clojure

{my/tag my.namespace/tag-handler}

It will also search for any :external:py:class:`importlib.metadata.EntryPoint` in the group ``basilisp_data_readers`` group.
Expand Down
10 changes: 10 additions & 0 deletions docs/runtime.rst
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,16 @@ See the documentation for :lpy:fn:`require` for more details.

: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`

.. _pythonpath_configuration:

``PYTHONPATH``, ``sys.path``, and Finding Basilisp Namespaces
#############################################################

Basilisp uses the ``PYTHONPATH`` environment variable and :external:py:data:`sys.path` to determine where to look for Basilisp code when requiring namespaces.
This is roughly analogous to the Java classpath in Clojure.
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.
These values may also be set via :ref:`cli` arguments.

.. _vars:

Vars
Expand Down
64 changes: 62 additions & 2 deletions src/basilisp/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import importlib.metadata
import io
import os
import pathlib
import sys
import textwrap
import types
Expand Down Expand Up @@ -78,6 +79,23 @@ def bootstrap_repl(ctx: compiler.CompilerContext, which_ns: str) -> types.Module
return importlib.import_module(REPL_NS)


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

def prepend_once(path: str) -> None:
if path in sys.path:
return
sys.path.insert(0, path)

for pth in args.include_path or []:
p = pathlib.Path(pth).resolve()
prepend_once(str(p))

if args.include_unsafe_path:
prepend_once(unsafe_path)


def _to_bool(v: Optional[str]) -> Optional[bool]:
"""Coerce a string argument to a boolean value, if possible."""
if v is None:
Expand Down Expand Up @@ -265,6 +283,39 @@ def _add_debug_arg_group(parser: argparse.ArgumentParser) -> None:
)


def _add_import_arg_group(parser: argparse.ArgumentParser) -> None:
group = parser.add_argument_group(
"path options",
description=(
"The path options below can be used to control how Basilisp (and Python) "
"find your code."
),
)
group.add_argument(
"--include-unsafe-path",
action="store",
nargs="?",
const=True,
default=os.getenv("BASILISP_INCLUDE_UNSAFE_PATH", "true"),
type=_to_bool,
help=(
"if true, automatically prepend a potentially unsafe path to `sys.path`; "
"setting `--include-unsafe-path=false` is the Basilisp equivalent to "
"setting PYTHONSAFEPATH to a non-empty string for CPython's REPL "
"(env: BASILISP_INCLUDE_UNSAFE_PATH; default: true)"
),
)
group.add_argument(
"-p",
"--include-path",
action="append",
help=(
"path to prepend to `sys.path`; may be specified more than once to "
"include multiple paths (env: PYTHONPATH)"
),
)


def _add_runtime_arg_group(parser: argparse.ArgumentParser) -> None:
group = parser.add_argument_group(
"runtime arguments",
Expand All @@ -281,8 +332,8 @@ def _add_runtime_arg_group(parser: argparse.ArgumentParser) -> None:
const=_to_bool(os.getenv("BASILISP_USE_DATA_READERS_ENTRY_POINT", "true")),
type=_to_bool,
help=(
"If true, Load data readers from importlib entry points in the "
'"basilisp_data_readers" group. (env: '
"if true, Load data readers from importlib entry points in the "
'"basilisp_data_readers" group (env: '
"BASILISP_USE_DATA_READERS_ENTRY_POINT; default: true)"
),
)
Expand Down Expand Up @@ -386,6 +437,7 @@ def nrepl_server(
args: argparse.Namespace,
) -> None:
basilisp.init(_compiler_opts(args))
init_path(args)
nrepl_server_mod = importlib.import_module(munge(NREPL_SERVER_NS))
nrepl_server_mod.start_server__BANG__(
lmap.map(
Expand Down Expand Up @@ -422,6 +474,7 @@ def _add_nrepl_server_subcommand(parser: argparse.ArgumentParser) -> None:
help='the file path where the server port number is output to, defaults to ".nrepl-port".',
)
_add_compiler_arg_group(parser)
_add_import_arg_group(parser)
_add_runtime_arg_group(parser)
_add_debug_arg_group(parser)

Expand All @@ -432,6 +485,7 @@ def repl(
) -> None:
opts = _compiler_opts(args)
basilisp.init(opts)
init_path(args)
ctx = compiler.CompilerContext(filename=REPL_INPUT_FILE_PATH, opts=opts)
prompter = get_prompter()
eof = object()
Expand Down Expand Up @@ -512,6 +566,7 @@ def _add_repl_subcommand(parser: argparse.ArgumentParser) -> None:
help="default namespace to use for the REPL",
)
_add_compiler_arg_group(parser)
_add_import_arg_group(parser)
_add_runtime_arg_group(parser)
_add_debug_arg_group(parser)

Expand Down Expand Up @@ -554,17 +609,21 @@ def run(
cli_args_var.bind_root(vec.vector(args.args))

if args.code:
init_path(args)
eval_str(target, ctx, ns, eof)
elif args.load_namespace:
# Set the requested namespace as the *main-ns*
main_ns_var = core_ns.find(sym.symbol(runtime.MAIN_NS_VAR_NAME))
assert main_ns_var is not None
main_ns_var.bind_root(sym.symbol(target))

init_path(args)
importlib.import_module(munge(target))
elif target == STDIN_FILE_NAME:
init_path(args)
eval_stream(io.TextIOWrapper(sys.stdin.buffer, encoding="utf-8"), ctx, ns)
else:
init_path(args, unsafe_path=str(pathlib.Path(target).resolve().parent))
eval_file(target, ctx, ns)


Expand Down Expand Up @@ -617,6 +676,7 @@ def _add_run_subcommand(parser: argparse.ArgumentParser) -> None:
help="command line args made accessible to the script as basilisp.core/*command-line-args*",
)
_add_compiler_arg_group(parser)
_add_import_arg_group(parser)
_add_runtime_arg_group(parser)
_add_debug_arg_group(parser)

Expand Down
Loading