Skip to content

Commit f933cc4

Browse files
committed
Re-work hab Formatter class to handle non-defined env vars
- The parse method now converts !e to literal_text when not expanding or if the env var is not set. - The get_field method now takes care of resolving existing env var's to strings. - Add the docs directory to .gitignore
1 parent 1f04de7 commit f933cc4

File tree

4 files changed

+81
-51
lines changed

4 files changed

+81
-51
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ __pycache__/
1111
build/
1212
coverage.xml
1313
dist/
14+
docs/
1415
htmlcov/
1516
venv/
1617
.venv/

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -880,7 +880,8 @@ with these extra features:
880880
* `{ANYTHING!e}`: `!e` is a special conversion flag for Environment variables. This will
881881
be replaced with the correct shell environment variable. For bash it becomes `$ANYTHING`,
882882
in power shell `$env:ANYTHING`, and in command prompt `%ANYTHING%`. `ANYTHING` is the name
883-
of the environment variable.
883+
of the environment variable. In some cases hab will expand these into the current
884+
environment variable if set, if the env var is not set the shell env variable will be used.
884885

885886
#### Hab specific variables
886887

hab/formatter.py

Lines changed: 53 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,27 @@
77
class Formatter(string.Formatter):
88
"""A extended string formatter class to parse habitat configurations.
99
10-
Adds support for the "!e" conversion flag. This will fill in the key as a properly
11-
formatted environment variable specifier. For example ''{PATH!e}'' will be converted
12-
to ``$PATH`` for the sh language, and ``%env:PATH`` for the ps language. You can
13-
convert "!e" to "!s" by setting expand to True. This simulates `os.path.expandvars`.
10+
Adds support for the ``!e`` `conversion field`_. This will fill in the key as a
11+
properly formatted environment variable specifier. For example ``{PATH!e}`` will
12+
be converted to ``$PATH`` for the sh language, and ``%env:PATH`` for the ps language.
13+
14+
By setting expand to True the ``!e`` conversion field will be filled in with the
15+
environment variable for that key if the env variable is set. Otherwise it will
16+
fall back the environment variable specifier.
1417
1518
This also converts ``{;}`` to the language specific path separator for environment
1619
variables. On linux this is ``:`` on windows(even in bash) this is ``;``.
1720
18-
You need to specify the desired language when initializing this class. This can be
19-
one of the supported language keys in ''Formatter.shell_formats'', or you can pass
20-
a file extension supported by `Formatter.language_from_ext`. If you pass None, it
21-
will preserve the formatting markers so it can be converted later.
21+
Parameters:
22+
language: Specify the shell language to use when converting ``!e``. See
23+
:py:const:`Formatter.shell_formats` and :py:meth:`Formatter.language_from_ext`.
24+
for supported values. If you pass None, it will preserve the
25+
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?
28+
29+
.. _conversion field:
30+
https://docs.python.org/3/library/string.html#grammar-token-format-string-conversion
2231
"""
2332

2433
shell_formats = {
@@ -50,28 +59,35 @@ class Formatter(string.Formatter):
5059
";": "{;}",
5160
},
5261
}
62+
"""Information on how to generate shell specific environment variable references
63+
and the character to use for pathsep. For each shell ``env_var`` is a format
64+
string that accepts the env var name. ``;`` is the path separator to use.
65+
"""
5366

5467
def __init__(self, language, expand=False):
5568
super().__init__()
5669
self.language = self.language_from_ext(language)
57-
self.current_field_name = None
5870
self.expand = expand
5971

60-
def convert_field(self, value, conversion):
61-
if conversion == "e":
62-
# Expand the env var to the real string value simulating `os.path.expandvars`
63-
if self.expand:
64-
return super().convert_field(value, "s")
72+
def get_field(self, field_name, args, kwargs):
73+
"""Returns the object to be inserted for the given field_name.
6574
66-
# Otherwise insert the correct shell script env var reference
67-
return self.shell_formats[self.language]["env_var"].format(
68-
self.current_field_name
69-
)
75+
If kwargs doesn't contain ``field_name`` but ``field_name`` is in the
76+
environment variables, the stored value is returned. This also returns
77+
the pathsep for the ``;`` field_name. Otherwise works the same as
78+
the standard `string.Formatter`_.
7079
71-
return super().convert_field(value, conversion)
80+
.. _`string.Formatter`:
81+
https://docs.python.org/3/library/string.html#string.Formatter.get_field
82+
"""
83+
# If a field_name was not provided, use the value stored in os.environ
84+
if field_name not in kwargs and field_name in os.environ:
85+
return os.getenv(field_name), field_name
86+
# Process the pathsep character
87+
if field_name == ";":
88+
value = self.shell_formats[self.language][";"]
89+
return value, field_name
7290

73-
def get_field(self, field_name, args, kwargs):
74-
self.current_field_name = field_name
7591
ret = super().get_field(field_name, args, kwargs)
7692
return ret
7793

@@ -97,16 +113,19 @@ def language_from_ext(cls, ext):
97113
return "sh"
98114
return ext
99115

100-
def merge_kwargs(self, kwargs):
101-
"""Merge the provided kwargs on top of the current language's shell_formats
102-
dict. This makes it so the default format options are added by default, but
103-
still allows us to override them if required
104-
"""
105-
ret = dict(self.shell_formats[self.language], **kwargs)
106-
ret = dict(os.environ, **ret)
107-
return ret
108-
109-
def vformat(self, format_string, args, kwargs):
110-
kwargs = self.merge_kwargs(kwargs)
111-
ret = super().vformat(format_string, args, kwargs)
112-
return ret
116+
def parse(self, txt):
117+
for literal_text, field_name, format_spec, conversion in super().parse(txt):
118+
# Non-hab specific operation, just use the super value unchanged
119+
if conversion != "e":
120+
yield (literal_text, field_name, format_spec, conversion)
121+
continue
122+
123+
elif self.expand and field_name in os.environ:
124+
# 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
128+
129+
# Convert this !e conversion to the shell specific env var specifier
130+
value = self.shell_formats[self.language]["env_var"].format(field_name)
131+
yield (literal_text + value, None, None, None)

tests/test_formatter.py

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import os
2-
31
import pytest
42

53
from hab import utils
@@ -8,28 +6,39 @@
86

97

108
@pytest.mark.parametrize(
11-
"language,shell,pathsep",
9+
"language,expanded,not_expanded",
1210
(
13-
("sh", "-$PATH-", "-:-"),
11+
("sh", "_V_a-var_:_$INVALID_", "_V_$VALID_:_$INVALID_"),
1412
# Bash formatting is different on windows for env vars
15-
("shwin", "-$PATH-", "-:-"),
16-
("ps", "-$env:PATH-", "-;-"),
17-
("batch", "-%PATH%-", "-;-"),
18-
(None, "-{PATH!e}-", "-{;}-"),
13+
("shwin", "_V_a-var_:_$INVALID_", "_V_$VALID_:_$INVALID_"),
14+
("ps", "_V_a-var_;_$env:INVALID_", "_V_$env:VALID_;_$env:INVALID_"),
15+
("batch", "_V_a-var_;_%INVALID%_", "_V_%VALID%_;_%INVALID%_"),
16+
(None, "_V_a-var_{;}_{INVALID!e}_", "_V_{VALID!e}_{;}_{INVALID!e}_"),
1917
),
2018
)
21-
def test_e_format(language, shell, pathsep):
22-
"""Check that "{VAR_NAME!e}" is properly formatted."""
23-
path = os.environ["PATH"]
19+
def test_env_format(language, expanded, not_expanded, monkeypatch):
20+
"""Check that the custom Formatter class works as expected.
21+
22+
This check tests:
23+
- You can still use pass kwargs like a normal Formatter.
24+
- ``!e`` is converted for set env vars if ``expand==True``.
25+
``VALID`` becomes ``a-var``.
26+
- ``!e`` uses the shell env specifier for set env vars if ``expand==False``.
27+
(``VALID`` becomes ``$VALID`` for bash.)
28+
- ``!e`` uses the shell env specifier for unset env variables.
29+
(``INVALID`` becomes ``$INVALID`` for bash.)
30+
- ``{;}`` gets converted to the shell's ``:`` or ``;``.
31+
"""
32+
monkeypatch.setenv("VALID", "a-var")
33+
monkeypatch.delenv("INVALID", raising=False)
34+
35+
fmt = "_{regular_var}_{VALID!e}_{;}_{INVALID!e}_"
2436

2537
# Check that "!e" is converted to the correct shell specific specifier.
26-
assert Formatter(language).format("-{PATH!e}-") == shell
38+
assert Formatter(language).format(fmt, regular_var="V") == not_expanded
2739

2840
# Check that "!e" uses the env var value if `expand=True` not the shell specifier.
29-
assert Formatter(language, expand=True).format("-{PATH!e}-") == f"-{path}-"
30-
31-
# Check that the pathsep variable `{;}` is converted to the correct value
32-
assert Formatter(language).format("-{;}-") == pathsep
41+
assert Formatter(language, expand=True).format(fmt, regular_var="V") == expanded
3342

3443

3544
def test_language_from_ext(monkeypatch):

0 commit comments

Comments
 (0)