Skip to content

Commit 4e3ab73

Browse files
committed
Improve environment variable expansion for python subprocess
This makes it so `Config.launch` should resolve the same environment variables as the `hab launch` cli. - Convert Formatters expand boolean into a mode Enum. It can now also replace undefined `!e` variables with nothing like shells do. - `Config.launch` uses the remove mode to replicate the shell.
1 parent 8cc2b7a commit 4e3ab73

File tree

4 files changed

+80
-16
lines changed

4 files changed

+80
-16
lines changed

hab/formatter.py

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
1+
import enum
12
import os
23
import string
34

45
from . import utils
56

67

8+
class ExpandMode(enum.Enum):
9+
"""Defines how Formatter replaces `!r` conformer flags."""
10+
11+
Preserve = (1, False)
12+
"""Preserve `!e` in Formatter calls. ('-{A!e}-' -> `-{A!e}-`)."""
13+
ToShell = (2, True)
14+
"""Convert `!e` to the shell's environment variable. ('-{A!e}-' -> `-$A-`)."""
15+
Remove = (3, True)
16+
"""Replace `!e` with a empty string. ('-{A!e}-' -> `--`)."""
17+
18+
def __init__(self, value, expand) -> None:
19+
self._value_ = value
20+
self.expand = expand
21+
22+
def __str__(self):
23+
return self.name
24+
25+
726
class Formatter(string.Formatter):
827
"""A extended string formatter class to parse habitat configurations.
928
@@ -23,8 +42,11 @@ class Formatter(string.Formatter):
2342
:py:const:`Formatter.shell_formats` and :py:meth:`Formatter.language_from_ext`.
2443
for supported values. If you pass None, it will preserve the
2544
formatting markers so it can be converted later.
26-
expand(bool, optional): Should the ``!e`` conversion insert the shell
27-
environment variable specifier or the value of the env var?
45+
expand(ExpandMode, optional): Controls how the ``!e`` conversion is handled.
46+
hab_env(dict, optional): If passed a dict of environment variable values
47+
defined and updated by hab. These are used to replace `!e` vars. If
48+
not found then falls back to os.environ values. This is used to expand
49+
a previously defined hab variable into another hab variable.
2850
2951
.. _conversion field:
3052
https://docs.python.org/3/library/string.html#grammar-token-format-string-conversion
@@ -64,10 +86,24 @@ class Formatter(string.Formatter):
6486
string that accepts the env var name. ``;`` is the path separator to use.
6587
"""
6688

67-
def __init__(self, language, expand=False):
89+
def __init__(self, language, expand=ExpandMode.Preserve, hab_env=None):
6890
super().__init__()
6991
self.language = self.language_from_ext(language)
7092
self.expand = expand
93+
self.hab_env = {} if hab_env is None else hab_env
94+
95+
def format(self, format_string, *args, hab_env=None, **kwargs):
96+
if hab_env is not None:
97+
# Use the hab_env defined values instead of os.environ if possible.
98+
try:
99+
current = self.hab_env
100+
self.hab_env = hab_env
101+
return super().format(format_string, *args, **kwargs)
102+
finally:
103+
self.hab_env = current
104+
else:
105+
# fallback to the os.environ and other defined kwargs.
106+
return super().format(format_string, *args, **kwargs)
71107

72108
def get_field(self, field_name, args, kwargs):
73109
"""Returns the object to be inserted for the given field_name.
@@ -80,6 +116,13 @@ def get_field(self, field_name, args, kwargs):
80116
.. _`string.Formatter`:
81117
https://docs.python.org/3/library/string.html#string.Formatter.get_field
82118
"""
119+
if field_name not in kwargs and field_name in self.hab_env:
120+
# if field_name was not provided, use value stored in self.hab_env
121+
value = self.hab_env[field_name]
122+
if isinstance(value, list) and len(value):
123+
value = utils.Platform.collapse_paths(value, ext=self.language)
124+
return value, field_name
125+
83126
# If a field_name was not provided, use the value stored in os.environ
84127
if field_name not in kwargs and field_name in os.environ:
85128
return os.getenv(field_name), field_name
@@ -120,12 +163,20 @@ def parse(self, txt):
120163
yield (literal_text, field_name, format_spec, conversion)
121164
continue
122165

123-
elif self.expand and field_name in os.environ:
166+
elif self.expand.expand:
124167
# Expand the env var to the env var value. Later `get_field`
125-
# will update kwargs with the existing env var value
126-
yield (literal_text, field_name, format_spec, "s")
127-
continue
168+
if field_name in self.hab_env:
169+
yield (literal_text, field_name, format_spec, "s")
170+
continue
171+
elif field_name in os.environ:
172+
# Expand the env var to the env var value. Later `get_field`
173+
# will update kwargs with the existing env var value
174+
yield (literal_text, field_name, format_spec, "s")
175+
continue
128176

129177
# Convert this !e conversion to the shell specific env var specifier
130-
value = self.shell_formats[self.language]["env_var"].format(field_name)
178+
if self.expand == ExpandMode.Remove:
179+
value = ""
180+
else:
181+
value = self.shell_formats[self.language]["env_var"].format(field_name)
131182
yield (literal_text + value, None, None, None)

hab/parsers/config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from .. import NotSet, utils
55
from ..errors import HabError, InvalidAliasError
6+
from ..formatter import ExpandMode
67
from .hab_base import HabBase
78
from .meta import hab_property
89

@@ -111,7 +112,7 @@ def launch(self, alias_name, args=None, blocking=False, cls=None, **kwargs):
111112
env = kwargs["env"]
112113
else:
113114
env = dict(os.environ)
114-
self.update_environ(env, alias_name)
115+
self.update_environ(env, alias_name, expand=ExpandMode.Remove)
115116
kwargs["env"] = env
116117

117118
# Launch the subprocess using the requested Popen subclass

hab/parsers/hab_base.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
InvalidAliasError,
1717
ReservedVariableNameError,
1818
)
19-
from ..formatter import Formatter
19+
from ..formatter import ExpandMode, Formatter
2020
from ..site import MergeDict
2121
from ..solvers import Solver
2222
from .meta import HabMeta, hab_property
@@ -757,7 +757,14 @@ def uri(self):
757757
return self._uri
758758
return self.fullpath
759759

760-
def update_environ(self, env, alias_name=None, include_global=True, formatter=None):
760+
def update_environ(
761+
self,
762+
env,
763+
alias_name=None,
764+
include_global=True,
765+
formatter=None,
766+
expand=ExpandMode.ToShell,
767+
):
761768
"""Updates the given environment variable dictionary to conform with
762769
the hab environment specification.
763770
@@ -772,17 +779,18 @@ def update_environ(self, env, alias_name=None, include_global=True, formatter=No
772779
also adds `HAB_FREEZE` if possible.
773780
formatter (hab.formatter.Formatter, optional): Str formatter class
774781
used to format the env var values.
782+
ext (str, optional): Defaults to `Platform.default_ext()` if NotSet.
775783
"""
776784
ext = utils.Platform.default_ext()
777785
if formatter is None:
778786
# Make sure to expand environment variables when formatting.
779-
formatter = Formatter(ext, expand=True)
787+
formatter = Formatter(ext, expand=expand)
780788

781789
def _apply(data):
782790
for key, value in data.items():
783791
if value:
784792
value = utils.Platform.collapse_paths(value, ext=ext, key=key)
785-
value = formatter.format(value, key=key, value=value)
793+
value = formatter.format(value, key=key, value=value, hab_env=env)
786794
env[key] = value
787795
else:
788796
env.pop(key, None)

tests/test_formatter.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pytest
22

33
from hab import utils
4-
from hab.formatter import Formatter
4+
from hab.formatter import ExpandMode, Formatter
55
from hab.parsers import Config
66

77

@@ -37,8 +37,12 @@ def test_env_format(language, expanded, not_expanded, monkeypatch):
3737
# Check that "!e" is converted to the correct shell specific specifier.
3838
assert Formatter(language).format(fmt, regular_var="V") == not_expanded
3939

40-
# Check that "!e" uses the env var value if `expand=True` not the shell specifier.
41-
assert Formatter(language, expand=True).format(fmt, regular_var="V") == expanded
40+
# Check that "!e" uses the env var value if `expand=ExpandMode.ToShell` not
41+
# the shell specifier.
42+
assert (
43+
Formatter(language, expand=ExpandMode.ToShell).format(fmt, regular_var="V")
44+
== expanded
45+
)
4246

4347

4448
def test_language_from_ext(monkeypatch):

0 commit comments

Comments
 (0)