Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Removed

- remove the unused CLI `--scope` option (#131). Use `ENVPATH` to select stack search directories; the Python API `scope` parameter remains supported.

- remove the legacy `envstack.path` API (`Path`, `Template`, `get_scope`, `get_template`, `match_template`, and `extract_fields`), its unused `env.Scope` wrapper, and path-only exceptions (#132). Use `pathlib` or `os.path` for filesystem operations and application-owned code for path templating; stack discovery continues to use `ENVPATH` and the current directory or explicit scope.

### Changed

- allow `-u`/`--unresolved` to select zero or more variables, matching `-r`/`--resolve`, with `-q` for a single value. Put stack names before the option: replace `envstack -u dev` with `envstack dev -u`.

### Added

- add NumPy, Pillow, and OpenEXR examples demonstrating accumulated search paths and runtime selection of NumPy builds
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ $ envstack
prod
```

To inspect selected raw values, use `envstack default -u PATH PYTHONPATH`.
Put stack names before `-u`, as with `-r`; omit variable names to show all values.

To inspect the unresolved environment (before variable expansion):

```bash
Expand Down
16 changes: 16 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ This is the programmatic equivalent of:
$ envstack default -u
```

Select raw values without expanding their references:

```bash
envstack default -u PATH PYTHONPATH
envstack default -u PYTHONPATH -q
```

Both `-u`/`--unresolved` and `-r`/`--resolve` accept zero or more variable
names. Without names they show all values; `-q` prints only the value of one
selected variable. Unknown variable names are omitted.

Put stack names before these options: use `envstack dev -u`, not
`envstack -u dev`. Arguments after `-u` now select variables, just as they do
after `-r`.


## Resolving a stack into concrete values

To evaluate variable references such as `${ROOT}/${ENV}`, resolve the loaded
Expand Down
3 changes: 3 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ $ envstack
prod
```

To inspect selected raw values, use `envstack default -u PATH PYTHONPATH`.
Put stack names before `-u`, as with `-r`; omit variable names to show all values.

Inspect the unresolved environment:

```bash
Expand Down
22 changes: 12 additions & 10 deletions lib/envstack/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,16 +218,13 @@ def parse_args():
metavar="KEY=VALUE",
help="overlay KEY=VALUE pairs to envstack environments",
)
parser.add_argument(
"--scope",
metavar="SCOPE",
help="search scope for environment stack files",
)
parser.add_argument(
"-u",
"--unresolved",
action="store_true",
help="dump unresolved environment variables to stdout",
nargs="*",
action=StoreOnce,
metavar="VAR",
help="show unresolved values for VARs (default: all); put STACKs before -u",
)
parser.add_argument(
"-r",
Expand Down Expand Up @@ -453,10 +450,15 @@ def main():
for source in env.sources:
print(source.path)

elif args.unresolved:
elif args.unresolved is not None:
env = load_environ(args.namespace, platform=args.platform, encrypt=args.encrypt)
for k, v in sorted(env.items(), key=lambda x: str(x[0])):
print(f"{k}={v}")
keys = args.unresolved or env.keys()
if args.quiet and len(keys) != 1:
print("error: --quiet requires exactly one KEY")
return 2
for key in sorted(str(k) for k in keys):
if key in env:
print(env[key] if args.quiet else f"{key}={env[key]}")

else:
return envshell(args.namespace, quiet=args.quiet)
Expand Down
58 changes: 58 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""CLI variable selection without launching a shell or installed executable."""

import sys

import pytest

from envstack import cli
from envstack import env as env_module


@pytest.fixture
def run_cli(tmp_path, monkeypatch, capsys):
(tmp_path / "sample.env").write_text("all:\n ROOT: /base\n LIB: ${ROOT}/lib\n EMPTY: ''\n")
monkeypatch.chdir(tmp_path)
monkeypatch.setenv("ENVPATH", str(tmp_path))
monkeypatch.setattr(env_module, "seen_stacks", set())

def run(*args):
monkeypatch.setattr(sys, "argv", ["envstack", *args])
status = cli.main()
return status or 0, capsys.readouterr().out

return run


@pytest.mark.parametrize("option", ["-u", "--unresolved"])
def test_unresolved_variable_selection(run_cli, option):
assert run_cli("sample", option, "LIB", "EMPTY") == (0, "EMPTY=\nLIB=${ROOT}/lib\n")


def test_unresolved_without_variables_keeps_full_output(run_cli):
status, output = run_cli("sample", "-u")
assert status == 0
assert "LIB=${ROOT}/lib\n" in output
assert "ROOT=/base\n" in output
assert "STACK=sample\n" in output


@pytest.mark.parametrize("option,expected", [("-u", "${ROOT}/lib\n"), ("-r", "/base/lib\n")])
def test_selected_value_quiet(run_cli, option, expected):
assert run_cli("sample", option, "LIB", "-q") == (0, expected)


def test_unresolved_quiet_rejects_multiple_variables(run_cli):
status, output = run_cli("sample", "-u", "ROOT", "LIB", "-q")
assert status == 2
assert output == "error: --quiet requires exactly one KEY\n"


@pytest.mark.parametrize("option", ["-u", "-r"])
def test_unknown_variable_is_omitted(run_cli, option):
assert run_cli("sample", option, "UNKNOWN") == (0, "")


def test_unresolved_repeated_option_rejected(run_cli):
with pytest.raises(SystemExit) as exc:
run_cli("sample", "-u", "LIB", "-u", "ROOT")
assert exc.value.code == 2
28 changes: 15 additions & 13 deletions tests/test_cmds.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class TestUnresolved(unittest.TestCase):
"""Tests unresolved environment variables from the cli."""

def setUp(self):
self.envstack_bin = "envstack -u"
self.envstack_bin = "envstack"
self.root = {
"linux": "/mnt/pipe",
"win32": "//tools/pipe",
Expand All @@ -99,7 +99,9 @@ def test_default(self):
"""
% self.root
)
output = subprocess.check_output(self.envstack_bin, shell=True, universal_newlines=True)
output = subprocess.check_output(
self.envstack_bin + " -u", shell=True, universal_newlines=True
)
self.assertEqual(output, expected_output)

def test_dev(self):
Expand All @@ -117,7 +119,7 @@ def test_dev(self):
"""
% self.root
)
command = "%s dev" % self.envstack_bin
command = "%s dev -u" % self.envstack_bin
output = subprocess.check_output(command, shell=True, universal_newlines=True)
self.assertEqual(output, expected_output)

Expand All @@ -137,7 +139,7 @@ def test_hello(self):
"""
% self.root
)
command = "%s hello" % self.envstack_bin
command = "%s hello -u" % self.envstack_bin
output = subprocess.check_output(command, shell=True, universal_newlines=True)
self.assertEqual(output, expected_output)

Expand All @@ -160,7 +162,7 @@ def test_thing(self):
TRUE=1
YES=1
"""
command = "%s thing" % self.envstack_bin
command = "%s thing -u" % self.envstack_bin
output = subprocess.check_output(command, shell=True, universal_newlines=True)
self.assertEqual(output, expected_output)

Expand All @@ -169,7 +171,7 @@ class TestEncrypt(unittest.TestCase):
"""Tests encrypting environment variables from the cli."""

def setUp(self):
self.envstack_bin = "envstack -u"
self.envstack_bin = "envstack"
self.root = {
"linux": "/mnt/pipe",
"win32": "//tools/pipe",
Expand Down Expand Up @@ -198,7 +200,7 @@ def test_default(self):
ROOT=L21udC9waXBl
STACK=ZGVmYXVsdA==
"""
command = "%s --encrypt" % self.envstack_bin
command = "%s -u --encrypt" % self.envstack_bin
output = subprocess.check_output(command, shell=True, universal_newlines=True)
self.assertEqual(output, expected_output)

Expand All @@ -207,7 +209,7 @@ def test_default_AESGCM(self):
encrypted values change every time."""
os.environ[AESGCMEncryptor.KEY_VAR_NAME] = AESGCMEncryptor.generate_key()
expected_output = f"""{self.root}/prod\n"""
command = "%s -e -- echo {DEPLOY_ROOT}" % self.envstack_bin
command = "%s -u -e -- echo {DEPLOY_ROOT}" % self.envstack_bin
output = subprocess.check_output(
command, shell=True, env=os.environ, universal_newlines=True
)
Expand All @@ -216,21 +218,21 @@ def test_default_AESGCM(self):
def test_default_resolve(self):
"""Get and encrypt default stack values, and test they are resolved in a subprocess."""
expected_output = f"""{self.root}/prod\n"""
command = "%s --encrypt -- echo {DEPLOY_ROOT}" % self.envstack_bin
command = "%s -u --encrypt -- echo {DEPLOY_ROOT}" % self.envstack_bin
output = subprocess.check_output(command, shell=True, universal_newlines=True)
self.assertEqual(output, expected_output)

def test_default_command_echo(self):
"""Tests that the default stack works with encrypted values."""
expected_output = f"""{self.root}/prod\n"""
command = "%s --encrypt -- echo {DEPLOY_ROOT}" % self.envstack_bin
command = "%s -u --encrypt -- echo {DEPLOY_ROOT}" % self.envstack_bin
output = subprocess.check_output(command, shell=True, universal_newlines=True)
self.assertEqual(output, expected_output)

def test_hello_command_echo(self):
"""Tests that resolved and encrypted values resolve in subprocesses."""
expected_output = "goodbye\n"
command = "%s thing -r HELLO -e -- echo {HELLO}" % self.envstack_bin
command = "%s thing -u -r HELLO -e -- echo {HELLO}" % self.envstack_bin
output = subprocess.check_output(command, shell=True, universal_newlines=True)
self.assertEqual(output, expected_output)

Expand All @@ -247,14 +249,14 @@ def test_dev(self):
ROOT=L21udC9waXBl
STACK=ZGV2
"""
command = "%s dev --encrypt" % self.envstack_bin
command = "%s dev -u --encrypt" % self.envstack_bin
output = subprocess.check_output(command, shell=True, universal_newlines=True)
self.assertEqual(output, expected_output)

def test_dev_command(self):
"""Tests that encrypted values resolve in subprocess in the dev stack."""
expected_output = f"""{self.root}/dev\n"""
command = "%s dev --encrypt -- echo {DEPLOY_ROOT}" % self.envstack_bin
command = "%s dev -u --encrypt -- echo {DEPLOY_ROOT}" % self.envstack_bin
output = subprocess.check_output(command, shell=True, universal_newlines=True)
self.assertEqual(output, expected_output)

Expand Down
Loading