Skip to content

Commit

Permalink
feat(app.ipython#99): respect CFG < ENV < CLI order
Browse files Browse the repository at this point in the history
  • Loading branch information
ankostis committed Aug 22, 2017
1 parent 86a72b7 commit 64eba1a
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 10 deletions.
2 changes: 1 addition & 1 deletion traitlets/config/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
#-----------------------------------------------------------------------------

CFG_RANK = 0
ENV_RANK = 10
ENV_RANK = 10 # Informative; used literal in :meth:`Configurable.update_config()`.
CLI_RANK = 20


Expand Down
10 changes: 9 additions & 1 deletion traitlets/config/configurable.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,15 @@ def _load_config(self, cfg, section_names=None, traits=None):
# hold trait notifications until after all config has been loaded
with self.hold_trait_notifications():
for name, config_value in my_config.items():
if name in traits:
trait = traits.get(name)
if trait:
env_value = trait.get_env_value()
## priority: file-conf < env-vars < cli-options
# See :data:`application.ENV_RANK`
if env_value is not None and my_config.rank_of(name) < 10:
setattr(self, name, env_value)
continue

if isinstance(config_value, LazyConfigValue):
# ConfigValue is a wrapper for using append / update on containers
# without having to copy the initial value
Expand Down
92 changes: 87 additions & 5 deletions traitlets/config/tests/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,17 @@

from traitlets.config.configurable import Configurable
from traitlets.config.loader import Config
from traitlets.tests.utils import get_output_error_code, check_help_output, check_help_all_output
from traitlets.tests.utils import (
get_output_error_code, check_help_output, check_help_all_output
)

from traitlets.config.application import (
Application
Application, CLI_RANK
)

from ipython_genutils.tempdir import TemporaryDirectory
from traitlets import (
default,
HasTraits,
Bool, Bytes, Unicode, Integer, List, Tuple, Set, Dict
)
Expand Down Expand Up @@ -430,7 +433,7 @@ def test_generate_config_file(self):
def test_generate_config_file_classes_to_include(self):
class NotInConfig(HasTraits):
from_hidden = Unicode('x', help="""From hidden class
Details about from_hidden.
""").tag(config=True)

Expand Down Expand Up @@ -623,7 +626,7 @@ def test_show_config(capsys):
cfg.MyApp.i = 5
# don't show empty
cfg.OtherApp

app = MyApp(config=cfg, show_config=True)
app.start()
out, err = capsys.readouterr()
Expand All @@ -636,14 +639,93 @@ def test_show_config_json(capsys):
cfg = Config()
cfg.MyApp.i = 5
cfg.OtherApp

app = MyApp(config=cfg, show_config_json=True)
app.start()
out, err = capsys.readouterr()
displayed = json.loads(out)
assert Config(displayed) == cfg


def test_env_vars_priority(monkeypatch):
class Conf(Configurable):
b = Unicode().tag(config=True, envvar='MY_ENVVAR')

@default('b')
def set_a_dyn(self):
return 'dyn'

class App(Application):
a = Unicode('def').tag(config=True, envvar='MY_ENVVAR')
aliases = {'a': 'App.a', 'b': 'Conf.b'}

def reconf(self):
self.conf = Conf(parent=self)

exp_no_envvar = {
'init': ('def', 'dyn'), # values after construction
'set': ('set', 'set'), # values after direct assignment
'cfg': ('cfg', 'cfg'), # values after `update_config()
'cli': ('cli', 'cli'), # values after prsing cmd-line args
}
exp_with_envvar = {
'init': ('env', 'env'),
'set': ('set', 'set'),
'cfg': ('env', 'env'),
'cli': ('cli', 'cli'),
}

def check_priority(exp):
import copy

cfg = Config()
cfg.App.a = cfg.Conf.b = 'cfg'

cfg_set = Config()
cfg_set.App.a = cfg_set.Conf.b = 'set'
cfg_set.set_default_rank(CLI_RANK + 10)

app = App(); app.reconf()
assert (app.a, app.conf.b) == exp['init']

app.update_config(cfg); app.reconf()
assert (app.a, app.conf.b) == exp['cfg']

app.update_config(cfg_set); app.reconf()
assert (app.a, app.conf.b) == exp['set']

## Above had been check by test_config.
## Now add cmd-line into the mix.

app.parse_command_line([]); app.reconf()
assert (app.a, app.conf.b) == exp['set'] # empty-cli do nothing.

app.parse_command_line('-a=cli -b=cli'.split()); app.reconf()
assert (app.a, app.conf.b) == exp['set'] # CLI lower than explict-set.

app = App(config=cfg); app.reconf()
assert (app.a, app.conf.b) == exp['cfg']

app.parse_command_line('-a=cli -b=cli'.split()); app.reconf()
assert (app.a, app.conf.b) == exp['cli']

app.update_config(cfg_set); app.reconf()
assert (app.a, app.conf.b) == exp['set']

app = App(); app.reconf()
assert (app.a, app.conf.b) == exp['init']

app.parse_command_line('-a=cli -b=cli'.split()); app.reconf()
assert (app.a, app.conf.b) == exp['cli']

app.update_config(cfg_set); app.reconf()
assert (app.a, app.conf.b) == exp['set']

check_priority(exp_no_envvar)
monkeypatch.setenv('MY_ENVVAR', 'env')
check_priority(exp_with_envvar)


if __name__ == '__main__':
# for test_help_output:
MyApp.launch_instance()
81 changes: 78 additions & 3 deletions traitlets/config/tests/test_configurable.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
)

from traitlets.traitlets import (
Integer, Float, Unicode, List, Dict, Set, Enum, FuzzyEnum,
CaselessStrEnum, _deprecations_shown, validate,
Integer, CInt, Float, Unicode, List, Dict, Set, Enum, FuzzyEnum,
CaselessStrEnum, _deprecations_shown, validate, default
)

from traitlets.config.loader import Config
Expand Down Expand Up @@ -311,7 +311,6 @@ class MyConf4(Configurable):
cls4_cfg.index(enum_choices_str))



class TestSingletonConfigurable(TestCase):

def test_instance(self):
Expand Down Expand Up @@ -670,3 +669,79 @@ class A(Configurable):
assert len(matches) == expected_nmatches
if expected_nmatches > 1:
assert matches[1].group(1) == '(only)'


def test_environment_variable_default(monkeypatch):
class A(Configurable):
b = CInt(allow_none=True).tag(config=True, envvar='MY_ENVVAR')

cfg = Config()
cfg.A.b = 2

a = A()
assert a.b == 0
a = A(config=cfg)
assert a.b == 2

monkeypatch.setenv('MY_ENVVAR', '1')

a = A()
assert a.b == 1 # env-var has precendance.

a = A(config=cfg)
assert a.b == 1 # env-var has precendance.

a.b = 3
assert a.b == 3 # Direct assignments override env-var.
a.b = None
assert a.b is None


def test_env_vars_priority(monkeypatch):
class Conf(Configurable):
a = Unicode('def').tag(config=True, envvar='MY_ENVVAR')
b = Unicode().tag(config=True, envvar='MY_ENVVAR')
aliases = {'def': 'App.a', 'dyn': 'App.b'}

@default('b')
def set_a_dyn(self):
return 'dyn'

exp_no_envvar = {
'init': ('def', 'dyn'), # values after "empty" construction
'set': ('set', 'set'), # values after direct assignment
'cfg': ('cfg', 'cfg'), # values after `update_config()
}
exp_with_envvar = {
'init': ('env', 'env'),
'set': ('set', 'set'),
'cfg': ('env', 'env'),
}

def check_priority(exp):
cfg = Config()
cfg.Conf.a = cfg.Conf.b = 'cfg'

conf = Conf()
assert (conf.a, conf.b) == exp['init']
conf.update_config(cfg)
assert (conf.a, conf.b) == exp['cfg']

conf.a = conf.b = 'set'
assert (conf.a, conf.b) == exp['set']

conf.update_config(cfg)
assert (conf.a, conf.b) == exp['cfg']

conf = Conf(a='set', b='set')
assert (conf.a, conf.b) == exp['set']

conf = Conf(config=cfg, a='set', b='set')
assert (conf.a, conf.b) == exp['set']

conf = Conf(config=cfg)
assert (conf.a, conf.b) == exp['cfg']

check_priority(exp_no_envvar)
monkeypatch.setenv('MY_ENVVAR', 'env')
check_priority(exp_with_envvar)

0 comments on commit 64eba1a

Please sign in to comment.