Skip to content

Commit 4cb88bf

Browse files
authored
Merge pull request #76 from rsgalloway/v0.9.2
Version 0.9.2
2 parents 04c71e3 + eb41c5c commit 4cb88bf

8 files changed

Lines changed: 154 additions & 105 deletions

File tree

dist.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
"source": "bin/*",
1010
"destination": "{DEPLOY_ROOT}/bin/%1"
1111
},
12-
"envstack_completion": {
12+
"bash_completion": {
1313
"source": "bin/envstack_completion.sh",
1414
"destination": "/etc/bash_completion.d/envstack"
1515
},
16-
"lib-envstack": {
16+
"lib": {
1717
"source": "lib/envstack",
1818
"destination": "{DEPLOY_ROOT}/lib/python/envstack"
1919
}

env/hello.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env envstack
2-
include: [default]
2+
include: [dev]
33
all: &all
44
LOG_LEVEL: ${LOG_LEVEL:=INFO}
55
PYEXE: /usr/bin/python

lib/envstack/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@
3434
"""
3535

3636
__prog__ = "envstack"
37-
__version__ = "0.9.1"
37+
__version__ = "0.9.2"
3838

3939
from envstack.env import clear, init, revert, save # noqa: F401

lib/envstack/cli.py

Lines changed: 63 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,19 @@ def parse_args():
147147
action="version",
148148
version=f"envstack {__version__}",
149149
)
150-
parser.add_argument(
150+
group = parser.add_mutually_exclusive_group(required=False)
151+
group.add_argument(
151152
"namespace",
152153
metavar="STACK",
153154
nargs="*",
154155
default=[config.DEFAULT_NAMESPACE],
155-
help="the environment stacks to use (default '%s')" % config.DEFAULT_NAMESPACE,
156+
help="the environment stacks to use",
157+
)
158+
group.add_argument(
159+
"-b",
160+
"--bare",
161+
action="store_true",
162+
help="create a bare environment",
156163
)
157164
encrypt_group = parser.add_argument_group("encryption options")
158165
encrypt_group.add_argument(
@@ -175,10 +182,11 @@ def parse_args():
175182
help="save the environment to an env file",
176183
)
177184
bake_group.add_argument(
185+
"-d",
178186
"--depth",
179187
type=int,
180188
default=0,
181-
help="depth of environment stack to bake",
189+
help="depth of environment stack to bake (default: 0 = flatten)",
182190
)
183191
parser.add_argument_group(bake_group)
184192
export_group = parser.add_argument_group("export options")
@@ -208,12 +216,6 @@ def parse_args():
208216
metavar="KEY=VALUE",
209217
help="overlay KEY=VALUE pairs to envstack environments",
210218
)
211-
parser.add_argument(
212-
"-b",
213-
"--bare",
214-
action="store_true",
215-
help="create a bare environment",
216-
)
217219
parser.add_argument(
218220
"--scope",
219221
metavar="SCOPE",
@@ -244,7 +246,7 @@ def parse_args():
244246
"-q",
245247
"--quiet",
246248
action="store_true",
247-
help="print values only",
249+
help="print the value of an environment variable only (no key)",
248250
)
249251

250252
args = parser.parse_args(args_before_dash)
@@ -287,7 +289,15 @@ def main():
287289
for key, value in data.items():
288290
print(f"{key}={value}")
289291

290-
elif args.set is not None:
292+
elif args.clear:
293+
from envstack.env import clear
294+
295+
print(clear(args.namespace, shell=config.SHELL))
296+
297+
elif args.export and args.resolve is None and args.set is None:
298+
print(export(args.namespace, shell=config.SHELL, encrypt=args.encrypt))
299+
300+
elif args.set is not None and args.resolve is None:
291301
force_stdin = args.set == [] or args.set == ["-"]
292302
using_pipe = args.set == [] and not sys.stdin.isatty()
293303

@@ -331,7 +341,7 @@ def main():
331341
if args.export:
332342
print(export_env_to_shell(env))
333343
elif args.out:
334-
Env(env).write(args.out)
344+
env.write(args.out, depth=args.depth)
335345
else:
336346
for key, val in env.items():
337347
if args.quiet:
@@ -343,30 +353,58 @@ def main():
343353
else:
344354
print(f"{key}={val}")
345355

346-
elif args.out:
356+
elif args.out and args.resolve is None:
347357
bake_environ(
348358
args.namespace,
349359
filename=args.out,
350-
depth=args.depth or 0,
360+
depth=args.depth,
351361
encrypt=args.encrypt,
352362
)
353363

354364
elif args.resolve is not None:
365+
if args.depth:
366+
print("error: --depth is not valid with --resolve")
367+
return 2
355368
resolved = resolve_environ(
356369
load_environ(args.namespace, platform=args.platform)
357370
)
358-
keys = args.resolve or resolved.keys()
359-
for key in sorted(str(k) for k in keys):
360-
val = resolved.get(key)
361-
if key in resolved:
362-
if args.quiet:
363-
if len(keys) > 1:
364-
print("error: --quiet requires exactly one KEY")
365-
return 2
371+
if args.set:
372+
resolved.update(_parse_keyvals(args.set))
373+
if args.encrypt:
374+
resolved = encrypt_environ(resolved)
375+
if args.out:
376+
if len(args.resolve) == 0:
377+
resolved.write(args.out, depth=0)
378+
else:
379+
keys = args.resolve or resolved.keys()
380+
if args.set:
381+
keys = set(keys).union(_parse_keyvals(args.set).keys())
382+
env = Env({key: resolved[key] for key in keys})
383+
env.write(args.out, depth=0)
384+
elif args.export:
385+
if len(args.resolve) == 0:
386+
print(export_env_to_shell(resolved, shell=config.SHELL))
387+
else:
388+
keys = args.resolve or resolved.keys()
389+
if args.set:
390+
keys = set(keys).union(_parse_keyvals(args.set).keys())
391+
env = Env({key: resolved[key] for key in keys})
392+
print(export_env_to_shell(env, shell=config.SHELL))
393+
else:
394+
keys = args.resolve or resolved.keys()
395+
if args.set:
396+
keys = set(keys).union(_parse_keyvals(args.set).keys())
397+
for key in sorted(str(k) for k in keys):
398+
val = resolved.get(key)
399+
if key in resolved:
400+
if args.quiet:
401+
if len(keys) > 1:
402+
print("error: --quiet requires exactly one KEY")
403+
return 2
404+
else:
405+
print(val)
366406
else:
367-
print(val)
368-
else:
369-
print(f"{key}={val}")
407+
print(f"{key}={val}")
370408

371409
elif args.trace is not None:
372410
if len(args.trace) == 0:
@@ -388,14 +426,6 @@ def main():
388426
for source in env.sources:
389427
print(source.path)
390428

391-
elif args.clear:
392-
from envstack.env import clear
393-
394-
print(clear(args.namespace, config.SHELL))
395-
396-
elif args.export:
397-
print(export(args.namespace, config.SHELL))
398-
399429
else:
400430
env = load_environ(
401431
args.namespace, platform=args.platform, encrypt=args.encrypt

lib/envstack/env.py

Lines changed: 53 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def length(self):
114114
"""Returns the char length of the path"""
115115
return len(self.path)
116116

117-
def load(self, platform=config.PLATFORM):
117+
def load(self, platform: str = config.PLATFORM):
118118
"""Reads .env from .path, and returns an Env class object"""
119119
if self.path and not self.data:
120120
self.data = load_file(self.path)
@@ -305,17 +305,12 @@ def bake(self, filename: str = None, depth: int = 0, encrypt: bool = False):
305305
>>> env.bake("baked.env")
306306
307307
:param filename: path to save the baked environment.
308-
:param depth: depth of source files to incldue (default: all).
309-
:param encrypt: encrypt the values.
308+
:param depth: depth of source files to include (optional).
309+
:param encrypt: encrypt the values (optional).
310310
:returns: baked environment.
311311
"""
312-
# get the sources for the given environment
313312
sources = self.sources
314-
315-
# look for encryption keys in the environment
316-
os.environ.update(get_keys_from_env(self))
317-
318-
# create a baked source
313+
os.environ.update(get_keys_from_env(self)) # for encryption
319314
baked = Source(filename)
320315

321316
def get_node_class(value):
@@ -327,35 +322,63 @@ def get_node_class(value):
327322
return EncryptedNode
328323
return value.__class__
329324

330-
# merge the sources into the outfile
325+
# track included files and seen keys
326+
includes = []
327+
seen_keys = set()
328+
329+
for source in sources[:depth]:
330+
for key, value in source.data.items():
331+
if isinstance(value, dict):
332+
for k, v in value.items():
333+
if k in self:
334+
v = self[k]
335+
node_class = get_node_class(v)
336+
seen_keys.add(k)
337+
else:
338+
seen_keys.add(key)
339+
340+
current_depth = 0
331341
for source in sources[-depth:]:
332342
for key, value in source.data.items():
343+
if key == "include" and current_depth <= depth:
344+
includes = value
345+
continue
333346
if isinstance(value, dict):
334347
for k, v in value.items():
348+
if k in self and key == "all": # override only in "all"
349+
v = self[k]
335350
node_class = get_node_class(v)
336351
baked.data.setdefault(key, {})[k] = node_class(v)
352+
seen_keys.add(k)
337353
else:
338-
node_class = get_node_class(value)
339-
baked.data[key] = node_class(value)
354+
baked.data[key] = get_node_class(value)(value)
355+
seen_keys.add(key)
356+
current_depth += 1
357+
358+
# add/override with values from the current environment
359+
for key, value in self.items():
360+
if key == "STACK" or key in seen_keys:
361+
continue
362+
baked.data["all"][key] = get_node_class(value)(value)
340363

341364
# clear includes if environment stack is fully baked
342-
if depth <= 0:
365+
if depth <= 0 or depth >= len(sources):
343366
baked.data["include"] = []
367+
else:
368+
baked.data["include"] = includes
344369

345-
# write the baked environment to the file
370+
# create the baked environment from the baked source
371+
baked_env = Env()
372+
baked_env.load_source(baked)
346373
if filename:
347374
try:
348375
baked.write()
349376
except Exception as err:
350377
raise WriteError(f"Failed to write {filename}", err)
351378

352-
# create the baked environment from the baked source
353-
baked_env = Env()
354-
baked_env.load_source(baked)
355-
356379
return baked_env
357380

358-
def write(self, filename: str = None):
381+
def write(self, filename: str, depth: int = 0, encrypt: bool = False):
359382
"""Writes the environment to an env file.
360383
361384
>>> env = Env({"FOO": "${BAR}", "BAR": "bar"})
@@ -367,11 +390,13 @@ def write(self, filename: str = None):
367390
>>> env.write("encrypted.env")
368391
369392
:param filename: path to save the baked environment.
393+
:param depth: depth of source files to include (optional).
394+
:param encrypt: encrypt the values (optional).
370395
:returns: Source object.
371396
"""
372397
# the environment was loaded from one or more sources
373398
if self.sources:
374-
baked = self.bake(filename)
399+
baked = self.bake(filename, depth=depth, encrypt=encrypt)
375400
return baked.sources[0]
376401

377402
# the environment was created from scratch
@@ -634,19 +659,21 @@ def export(
634659
name: str = config.DEFAULT_NAMESPACE,
635660
shell: str = config.SHELL,
636661
scope: str = None,
662+
encrypt: bool = False,
637663
):
638664
"""Returns shell commands that can be sourced to set environment stack
639665
environment variables.
640666
641667
Supported shells: bash, sh, tcsh, cmd, pwsh (see config.detect_shell()).
642668
643669
:param name: stack namespace.
644-
:param shell: name of shell (default: current shell).
645-
:param scope: environment scope (default: cwd).
670+
:param shell: name of shell (optional).
671+
:param scope: environment scope (optional).
672+
:param encrypt: encrypt the values (optional).
646673
:returns: shell commands as string.
647674
"""
648-
resolved_env = resolve_environ(load_environ(name, scope=scope))
649-
return export_env_to_shell(resolved_env, shell)
675+
env = load_environ(name, scope=scope, encrypt=encrypt)
676+
return export_env_to_shell(env, shell)
650677

651678

652679
def save():
@@ -747,8 +774,8 @@ def bake_environ(
747774
$ envstack [STACK] -o <filename>
748775
749776
:param name: stack namespace.
750-
:param scope: environment scope (default: cwd).
751-
:param depth: depth of source files to incldue (default: all).
777+
:param scope: environment scope (optional).
778+
:param depth: depth of source files to include (optional).
752779
:param filename: path to save the baked environment.
753780
:param encrypt: encrypt the values.
754781
:returns: baked environment.

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040

4141
setup(
4242
name="envstack",
43-
version="0.9.1",
43+
version="0.9.2",
4444
description="Stacked environment variable management system",
4545
long_description=long_description,
4646
long_description_content_type="text/markdown",

0 commit comments

Comments
 (0)