Skip to content

Commit ce44800

Browse files
authored
Merge pull request #28 from mkaranasou/issue_26_dtype_support
Tag with datatype support
2 parents fcc7706 + 783c6f0 commit ce44800

3 files changed

Lines changed: 97 additions & 5 deletions

File tree

README.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ pip install pyaml-env
2121
#### Basic Usage: Environment variable parsing
2222
This yaml file:
2323
```yaml
24-
databse:
24+
database:
2525
name: test_db
2626
username: !ENV ${DB_USER}
2727
password: !ENV ${DB_PASS}
@@ -99,7 +99,37 @@ print(config)
9999
**NOTE 1**: If you set `tag` to `None`, then, the current behavior is that environment variables in all places in the yaml will be resolved (if set).
100100

101101
---
102+
#### Datatype parsing with yaml's tag:yaml.org,2002:<datatype>
102103

104+
```python
105+
# because this is not allowed:
106+
# data1: !TAG !!float ${ENV_TAG2:27017}
107+
# use tag:yaml.org,2002:datatype to convert value:
108+
test_data = '''
109+
data0: !TAG ${ENV_TAG1}
110+
data1: !TAG tag:yaml.org,2002:float ${ENV_TAG2:27017}
111+
data2: !!float 1024
112+
data3: !TAG ${ENV_TAG2:some_value}
113+
data4: !TAG tag:yaml.org,2002:bool ${ENV_TAG2:false}
114+
'''
115+
```
116+
Will become:
117+
```python
118+
os.environ['ENV_TAG1'] = "1024"
119+
config = parse_config(data=test_data, tag='!TAG')
120+
print(config)
121+
{
122+
'data0': '1024',
123+
'data1': 27017.0,
124+
'data2': 1024.0,
125+
'data3': 'some_value',
126+
'data4': False
127+
}
128+
```
129+
130+
[reference in yaml code](https://github.com/yaml/pyyaml/blob/master/lib/yaml/parser.py#L78)
131+
132+
---
103133
#### If nothing matches: `N/A` as `default_value`:
104134

105135
If no defaults are found and no environment variables, the `default_value` (**which is `N/A` by default**) is used:

src/pyaml_env/parse_config.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ def parse_config(
5151

5252
# the tag will be used to mark where to start searching for the pattern
5353
# e.g. a_key: !ENV somestring${ENV_VAR}other_stuff_follows
54-
loader.add_implicit_resolver(tag, pattern, None)
54+
loader.add_implicit_resolver(tag, pattern, first=[tag])
55+
56+
# For inner type conversions because double tags do not work, e.g. !ENV !!float
57+
type_tag = 'tag:yaml.org,2002:'
58+
type_tag_pattern = re.compile(f'({type_tag}\w+\s)')
5559

5660
def constructor_env_variables(loader, node):
5761
"""
@@ -65,13 +69,15 @@ def constructor_env_variables(loader, node):
6569
"""
6670
value = loader.construct_scalar(node)
6771
match = pattern.findall(value) # to find all env variables in line
72+
dt = ''.join(type_tag_pattern.findall(value)) or ''
73+
value = value.replace(dt, '')
6874
if match:
6975
full_value = value
7076
for g in match:
7177
curr_default_value = default_value
7278
env_var_name = g
7379
env_var_name_with_default = g
74-
if default_sep and isinstance(g, tuple) and len(g) > 1:
80+
if default_sep and isinstance(g, tuple) and len(g) > 1:
7581
env_var_name = g[0]
7682
env_var_name_with_default = ''.join(g)
7783
found = False
@@ -88,7 +94,13 @@ def constructor_env_variables(loader, node):
8894
f'${{{env_var_name_with_default}}}',
8995
os.environ.get(env_var_name, curr_default_value)
9096
)
97+
if dt:
98+
# do one more roundtrip with the dt constructor:
99+
node.value = full_value
100+
node.tag = dt.strip()
101+
return loader.yaml_constructors[node.tag](loader, node)
91102
return full_value
103+
92104
return value
93105

94106
loader.add_constructor(tag, constructor_env_variables)

tests/pyaml_env_tests/test_parse_config.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,31 @@ def test_parse_config_different_tag_error(self):
692692
in str(ce.exception.problem)
693693
)
694694

695+
def test_parse_config_only_tag_env_vars_resolved(self):
696+
os.environ[self.env_var1] = 'it works!'
697+
os.environ[self.env_var2] = 'this works too!'
698+
test_data = '''
699+
test1:
700+
data0: !TEST2 test1${ENV_TAG1}
701+
data1: ${ENV_TAG2}
702+
'''
703+
704+
expected = {
705+
'test1': {
706+
'data0': f'test1{os.environ[self.env_var1]}',
707+
'data1': '${ENV_TAG2}'
708+
}
709+
}
710+
711+
# because None is used as a tag in one of the tests, it messes up expected behavior
712+
# remove None from implicit resolvers:
713+
if None in yaml.SafeLoader.yaml_implicit_resolvers:
714+
del yaml.SafeLoader.yaml_implicit_resolvers[None]
715+
716+
# only ENV_TAG1 should be parsed because of !TEST2 tag
717+
result = parse_config(data=test_data, tag='!TEST2')
718+
self.assertDictEqual(result, expected)
719+
695720
def test_parse_config_no_tag_all_resolved(self):
696721
os.environ[self.env_var1] = 'it works!'
697722
os.environ[self.env_var2] = 'this works too!'
@@ -704,9 +729,34 @@ def test_parse_config_no_tag_all_resolved(self):
704729
expected = {
705730
'test1': {
706731
'data0': f'test1{os.environ[self.env_var1]}',
707-
'data1': os.environ[self.env_var2]
732+
'data1': 'this works too!'
708733
}
709734
}
735+
# all environment variables will be parsed
710736
result = parse_config(data=test_data, tag=None)
737+
self.assertDictEqual(result, expected)
738+
739+
self.assertDictEqual(result, expected)
711740

712-
self.assertDictEqual(result, expected)
741+
def test_numeric_values_with_type_defined(self):
742+
os.environ[self.env_var1] = "1024"
743+
744+
test_data = '''
745+
data0: !TAG ${ENV_TAG1}
746+
data1: !TAG tag:yaml.org,2002:float ${ENV_TAG2:27017}
747+
data2: !!float 1024
748+
data3: !TAG ${ENV_TAG2:some_value}
749+
data4: !TAG tag:yaml.org,2002:bool ${ENV_TAG2:false}
750+
'''
751+
config = parse_config(data=test_data, tag='!TAG')
752+
print(config)
753+
self.assertIsInstance(config['data2'], float)
754+
self.assertIsInstance(config['data3'], str)
755+
self.assertIsInstance(config['data1'], float)
756+
self.assertIsInstance(config['data4'], bool)
757+
self.assertIsInstance(config['data0'], str)
758+
self.assertEqual(config['data0'], os.environ[self.env_var1])
759+
self.assertEqual(config['data2'], float(os.environ[self.env_var1]))
760+
self.assertEqual(config['data1'], 27017.0)
761+
self.assertEqual(config['data3'], "some_value")
762+
self.assertEqual(config['data4'], False)

0 commit comments

Comments
 (0)