Repository: ipython/traitlets
Pull request #582 (2020-06-25 07:43:46)

Add trait.from_string
Re-issue of long-idle #441

The key to this PR is deferring the parsing of strings from the CLI to trait objects themselves, to avoid inconsistency when

This is attempting to resolve an inconsistency in the improved argparse handling for container traits, where a list trait can be specified in the argparse list way of giving the flag multiple times:

```
command -l x -l y
# or
command --Class.list_trait x --Class.list_trait y
```

which results in `Class.list_trait = ['x', 'y']`

The main issue with master is summarized [here](https://github.com/ipython/traitlets/pull/441#issuecomment-323811638):

>  There are three cases: `--short` aliases, `--Class.trait` in the classes list, and `--Class.trait` not in the classes list.

The new handling works for `--short` and when `Class` is in the classes list, but *not* when `Class` is not in the explicit Application.classes list (e.g. an IPython extension). This PR so far improves handling of scalars (solving the longstanding `ipython -c 1` bug), but container traits have been the sticky part.

TODO:

- [x] add TraitType.from_string
- [x] define from_string for simple scalars
- [x] use trait.from_string to evaluate CLI strings at config-loading time instead of CLI-parsing time
- [x] define from_string for container traits (`item_from_string`?)
- [x] make `--Class.trait x --Class.trait y` produce the same config regardless of `Application.classes`
- [x] test confirming above
- [ ] test some sensible Union cases

minrk: @Carreau I pushed a commit that makes handling containers more consistent. Tests need to be updated and several more cases covered, but it should be mostly there.

The main sketchy thing I did to accomplish this was to load a private-api-patched ArgumentParser to enable defining `--Class.trait` options on-demand. The goal here was to ensure consistent handling of `--Foo.bar=x`, `--Foo.bar x`, etc. If you have a better solution to that part, I'd be happy to work through it.

Another question to resolve is where deprecated literal-eval handling belongs (`--string "'1'"` and `--dict '{"key": "value"}'`). The easiest way to make sure it's always called is to put it in the new `from_string` itself, but it logically makes the most sense to only apply it when coming from CLI.
Carreau: I think that the changes of behavior of Cli parsing between 4.x and 5.0 is fair game especially if it's in corner case. It seem fair to say that too complex of command line may not work well. 

It's also common to have regression in major releases that are fixed in the next patched version. 

I'll try to find some cycles to write test and understand what you wrote. 
Carreau: @minrk can you look at https://github.com/minrk/traitlets/pull/1  (A PR against your PR, likely easier to review). 

Patch:
diff --git a/.gitignore b/.gitignore
index 5c348b0..8c8170e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,4 @@ __pycache__
 .#*
 .coverage
 .cache
+htmlcov
diff --git a/traitlets/config/application.py b/traitlets/config/application.py
index 0728014..abf697c 100644
--- a/traitlets/config/application.py
+++ b/traitlets/config/application.py
@@ -661,6 +661,7 @@ class Application(SingletonConfigurable):
     @catch_config_error
     def parse_command_line(self, argv=None):
         """Parse the command line arguments."""
+        assert not isinstance(argv, str)
         argv = sys.argv[1:] if argv is None else argv
         self.argv = [cast_unicode(arg) for arg in argv ]
 
@@ -693,10 +694,15 @@ class Application(SingletonConfigurable):
             self.exit(0)
 
         # flatten flags&aliases, so cl-args get appropriate priority:
-        flags,aliases = self.flatten_flags()
+        flags, aliases = self.flatten_flags()
         classes = tuple(self._classes_with_config_traits())
         loader = self._create_loader(argv, aliases, flags, classes=classes)
-        self.cli_config = deepcopy(loader.load_config())
+        try:
+            self.cli_config = deepcopy(loader.load_config())
+        except SystemExit:
+            # print help output on error
+            self.print_help()
+            raise
         self.update_config(self.cli_config)
         # store unparsed args in extra_args
         self.extra_args = loader.extra_args
diff --git a/traitlets/config/configurable.py b/traitlets/config/configurable.py
index 6e3120d..9d7e184 100644
--- a/traitlets/config/configurable.py
+++ b/traitlets/config/configurable.py
@@ -7,7 +7,7 @@
 from copy import deepcopy
 import warnings
 
-from .loader import Config, LazyConfigValue, _is_section_key
+from .loader import Config, LazyConfigValue, DeferredConfig, _is_section_key
 from traitlets.traitlets import (
     HasTraits,
     Instance,
@@ -163,6 +163,9 @@ class Configurable(HasTraits):
                         # without having to copy the initial value
                         initial = getattr(self, name)
                         config_value = config_value.get_value(initial)
+                    elif isinstance(config_value, DeferredConfig):
+                        # DeferredConfig tends to come from CLI/environment variables
+                        config_value = config_value.get_value(traits[name])
                     # We have to do a deepcopy here if we don't deepcopy the entire
                     # config object. If we don't, a mutable config_value will be
                     # shared by all instances, effectively making it a class attribute.
@@ -302,9 +305,9 @@ class Configurable(HasTraits):
 
         Parameters
         ----------
-        trait: Trait
+        trait : Trait
             The trait to look for
-        classes: list
+        classes : list
             The list of other classes to consider for redundancy.
             Will return `cls` even if it is not defined on `cls`
             if the defining class is not in `classes`.
@@ -323,7 +326,7 @@ class Configurable(HasTraits):
 
         Parameters
         ----------
-        classes: list, optional
+        classes : list, optional
             The list of other classes in the config file.
             Used to reduce redundant information.
         """
diff --git a/traitlets/config/loader.py b/traitlets/config/loader.py
index ff20ae1..11599ea 100644
--- a/traitlets/config/loader.py
+++ b/traitlets/config/loader.py
@@ -10,7 +10,9 @@ import os
 import re
 import sys
 import json
+import warnings
 from ast import literal_eval
+from collections import defaultdict
 
 from ..utils import cast_unicode
 
@@ -47,6 +49,18 @@ class ArgumentError(ConfigLoaderError):
 # to do.  So we override the print_help method with one that defaults to
 # stdout and use our class instead.
 
+
+class _Sentinel:
+    def __repr__(self):
+        return "<Sentinel deprecated>"
+
+    def __str__(self):
+        return "<deprecated>"
+
+
+_deprecated = _Sentinel()
+
+
 class ArgumentParser(argparse.ArgumentParser):
     """Simple argparse subclass that prints help to stdout by default."""
 
@@ -102,11 +116,11 @@ class LazyConfigValue(HasTraits):
 
         Parameters
         ----------
-        other: LazyConfigValue or container
+        other : LazyConfigValue or container
 
         Returns
         -------
-        LazyConfigValue 
+        LazyConfigValue
             if ``other`` is also lazy, a reified container otherwise.
         """
         if isinstance(other, LazyConfigValue):
@@ -126,8 +140,6 @@ class LazyConfigValue(HasTraits):
             # other is a container, reify now.
             return self.get_value(other)
 
-
-
     def insert(self, index, other):
         if not isinstance(index, int):
             raise TypeError("An integer is required")
@@ -136,6 +148,7 @@ class LazyConfigValue(HasTraits):
     # dict methods
     # update is used for both dict and set
     _update = Any()
+
     def update(self, other):
         if self._update is None:
             if isinstance(other, dict):
@@ -346,6 +359,87 @@ class Config(dict):
             raise AttributeError(e)
 
 
+class DeferredConfig:
+    """Class for deferred-evaluation of config from CLI"""
+    pass
+
+    def get_value(self, trait):
+        raise NotImplementedError("Implement in subclasses")
+
+
+class DeferredConfigString(str, DeferredConfig):
+    """Config value for loading config from a string
+
+    Interpretation is deferred until it is loaded into the trait.
+
+    Subclass of str for backward compatibility.
+
+    This class is only used for values that are not listed
+    in the configurable classes.
+
+    When config is loaded, `trait.from_string` will be used.
+
+    If an error is raised in `.from_string`,
+    the original string is returned.
+
+    .. versionadded:: 5.0
+    """
+    def get_value(self, trait):
+        """Get the value stored in this string"""
+        s = str(self)
+        try:
+            return trait.from_string(s)
+        except Exception:
+            # exception casting from string,
+            # let the original string lie.
+            # this will raise a more informative error when config is loaded.
+            return s
+
+    def __repr__(self):
+        super_repr = super(DeferredConfigString, self).__repr__()
+        return '%s(%s)' % (self.__class__.__name__, super_repr)
+
+
+class DeferredConfigList(list, DeferredConfig):
+    """Config value for loading config from a list of strings
+
+    Interpretation is deferred until it is loaded into the trait.
+
+    This class is only used for values that are not listed
+    in the configurable classes.
+
+    When config is loaded, `trait.from_string_list` will be used.
+
+    If an error is raised in `.from_string_list`,
+    the original string list is returned.
+
+    .. versionadded:: 5.0
+    """
+    def get_value(self, trait):
+        """Get the value stored in this string"""
+        if hasattr(trait, "from_string_list"):
+            src = list(self)
+            cast = trait.from_string_list
+        else:
+            # only allow one item
+            if len(self) > 1:
+                raise ValueError("Multiply defined option: %s" % trait.name)
+            src = self[0]
+            cast = trait.from_string
+
+        try:
+            return cast(src)
+        except Exception:
+            # exception casting from string,
+            # let the original value lie.
+            # this will raise a more informative error when config is loaded.
+            return src
+
+    def __repr__(self):
+        super_repr = super(DeferredConfigList, self).__repr__()
+        return '%s(%s)' % (self.__class__.__name__, super_repr)
+
+
 #-----------------------------------------------------------------------------
 # Config loading classes
 #-----------------------------------------------------------------------------
@@ -374,7 +468,7 @@ class ConfigLoader(object):
         """A base class for config loaders.
 
         log : instance of :class:`logging.Logger` to use.
-              By default loger of :meth:`traitlets.config.application.Application.instance()`
+              By default logger of :meth:`traitlets.config.application.Application.instance()`
               will be used
 
         Examples
@@ -544,240 +638,126 @@ class CommandLineConfigLoader(ConfigLoader):
     here.
     """
 
-    def _parse_config_value(self, rhs):
-        """Python-evaluates any cmd-line argument values."""
-        rhs = os.path.expanduser(rhs)
-        try:
-            # Try to see if regular Python syntax will work. This
-            # won't handle strings as the quote marks are removed
-            # by the system shell.
-            value = literal_eval(rhs)
-        except (NameError, SyntaxError, ValueError):
-            # This case happens if the rhs is a string.
-            value = rhs
-        return value
-
     def _exec_config_str(self, lhs, rhs, trait=None):
         """execute self.config.<lhs> = <rhs>
 
         * expands ~ with expanduser
-        * tries to assign with literal_eval, otherwise assigns with just the string,
-          allowing `--C.a=foobar` and `--C.a="foobar"` to be equivalent.  *Not*
-          equivalent are `--C.a=4` and `--C.a='4'`.
+        * interprets value with trait if available
         """
-        if isinstance(trait, Dict):
-            if len(rhs) == 1 and isinstance(rhs[0], str):
-                # check for deprecated --Class.trait="{'a': 'b'}"
-                self.log.warning(
-                    "--{0}={1} for dict-traits is deprecated in traitlets 5.0. "
-                    "You can pass --{0} <key=value> ... multiple times to add items to a dict.".format(
-                        lhs, rhs[0])
-                )
-                value = self._parse_config_value(rhs[0])
-            else:
-                value = {k: self._parse_config_value(v) for k,v in rhs}
-
-        elif isinstance(rhs, (list, tuple)):
-            value = None
-            if len(rhs) == 1 and len(rhs[0]) > 0:
-                # check for deprecated --Class.trait="['a', 'b', 'c']"
-                r = rhs[0]
-                if (
-                    (r[0] == '[' and r[-1] == ']') or
-                    (r[0] == '(' and r[-1] == ')')
-                ):
-                    self.log.warning(
-                        "--{0}={1} for containers is deprecated in traitlets 5.0. "
-                        "You can pass --{0} item ... multiple times to add items to a list.".format(
-                            lhs, rhs)
-                    )
-                    value = self._parse_config_value(r)
-
-            if value is None:
-                value = [self._parse_config_value(r) for r in rhs]
+        value = rhs
+        if isinstance(value, DeferredConfig):
+            if trait:
+                # trait available, reify config immediately
+                value = value.get_value(trait)
+            elif isinstance(rhs, DeferredConfigList) and len(rhs) == 1:
+                # single item, make it a deferred str
+                value = DeferredConfigString(os.path.expanduser(rhs[0]))
         else:
-            value = self._parse_config_value(rhs)
+            if trait:
+                value = trait.from_string(value)
+            else:
+                value = DeferredConfigString(value)
 
-        exec(u'self.config.%s = value' % lhs, None, locals())
+        *path, key = lhs.split(".")
+        section = self.config
+        for part in path:
+            section = section[part]
+        section[key] = value
+        return
 
     def _load_flag(self, cfg):
         """update self.config from a flag, which can be a dict or Config"""
         if isinstance(cfg, (dict, Config)):
             # don't clobber whole config sections, update
             # each section from config:
-            for sec,c in cfg.items():
+            for sec, c in cfg.items():
                 self.config[sec].update(c)
         else:
             raise TypeError("Invalid flag: %r" % cfg)
 
-# raw --identifier=value pattern
-# but *also* accept '-' as wordsep, for aliases
-# accepts:  --foo=a
-#           --Class.trait=value
-#           --alias-name=value
-# rejects:  -foo=value
-#           --foo
-#           --Class.trait
-kv_pattern = re.compile(r'\-\-[A-Za-z][\w\-]*(\.[\w\-]+)*\=.*')
+# match --Class.trait keys for argparse
+# matches:
+# --Class.trait
+# --x
+# -x
 
-# just flags, no assignments, with two *or one* leading '-'
-# accepts:  --foo
-#           -foo-bar-again
-# rejects:  --anything=anything
-#           --two.word
+class_trait_opt_pattern = re.compile(r'^\-?\-[A-Za-z][\w]*(\.[\w]+)*$')
 
-flag_pattern = re.compile(r'\-\-?\w+[\-\w]*$')
+_DOT_REPLACEMENT = "__DOT__"
+_DASH_REPLACEMENT = "__DASH__"
 
-class KeyValueConfigLoader(CommandLineConfigLoader):
-    """A config loader that loads key value pairs from the command line.
 
-    This allows command line options to be gives in the following form::
+class _KVAction(argparse.Action):
+    """Custom argparse action for handling --Class.trait=x
 
-        ipython --profile="foo" --InteractiveShell.autocall=False
+    Always
     """
-
-    def __init__(self, argv=None, aliases=None, flags=None, **kw):
-        """Create a key value pair config loader.
-
-        Parameters
-        ----------
-        argv : list
-            A list that has the form of sys.argv[1:] which has unicode
-            elements of the form u"key=value". If this is None (default),
-            then sys.argv[1:] will be used.
-        aliases : dict
-            A dict of aliases for configurable traits.
-            Keys are the short aliases, Values are the resolved trait.
-            Of the form: `{'alias' : 'Configurable.trait'}`
-        flags : dict
-            A dict of flags, keyed by str name. Vaues can be Config objects,
-            dicts, or "key=value" strings.  If Config or dict, when the flag
-            is triggered, The flag is loaded as `self.config.update(m)`.
-
-        Returns
-        -------
-        config : Config
-            The resulting Config object.
-
-        Examples
-        --------
-        >>> from traitlets.config.loader import KeyValueConfigLoader
-        >>> cl = KeyValueConfigLoader()
-        >>> d = cl.load_config(["--A.name='brian'", "--B.number=0"])
-        >>> sorted(d.items())
-        [('A', {'name': 'brian'}), ('B', {'number': 0})]
-        """
-        super(KeyValueConfigLoader, self).__init__(**kw)
-        if argv is None:
-            argv = sys.argv[1:]
-        self.argv = argv
-        self.aliases = aliases or {}
-        self.flags = flags or {}
+    def __call__(self, parser, namespace, values, option_string=None):
+        if isinstance(values, str):
+            values = [values]
+        values = ["-" if v is _DASH_REPLACEMENT else v for v in values]
+        items = getattr(namespace, self.dest, None)
+        if items is None:
+            items = DeferredConfigList()
+        else:
+            items = DeferredConfigList(items)
+        items.extend(values)
+        setattr(namespace, self.dest, items)
 
 
-    def clear(self):
-        super(KeyValueConfigLoader, self).clear()
-        self.extra_args = []
+class _DefaultOptionDict(dict):
+    """Like the default options dict
 
+    but acts as if all --Class.trait options are predefined
+    """
+    def _add_kv_action(self, key):
+        self[key] = _KVAction(
+            option_strings=[key],
+            dest=key.lstrip("-").replace(".", _DOT_REPLACEMENT),
+            # use metavar for display purposes
+            metavar=key.lstrip("-"),
+        )
 
-    def _decode_argv(self, argv, enc=None):
-        """decode argv if bytes, using stdin.encoding, falling back on default enc"""
-        uargv = []
-        if enc is None:
-            enc = DEFAULT_ENCODING
-        for arg in argv:
-            if not isinstance(arg, str):
-                # only decode if not already decoded
-                arg = arg.decode(enc)
-            uargv.append(arg)
-        return uargv
-
+    def __contains__(self, key):
+        if '=' in key:
+            return False
+        if super().__contains__(key):
+            return True
+
+        if key.startswith("-") and class_trait_opt_pattern.match(key):
+            self._add_kv_action(key)
+            return True
+        return False
 
-    def load_config(self, argv=None, aliases=None, flags=None):
-        """Parse the configuration and generate the Config object.
+    def __getitem__(self, key):
+        if key in self:
+            return super().__getitem__(key)
+        else:
+            raise KeyError(key)
 
-        After loading, any arguments that are not key-value or
-        flags will be stored in self.extra_args - a list of
-        unparsed command-line arguments.  This is used for
-        arguments such as input files or subcommands.
+    def get(self, key, default=None):
+        try:
+            return self[key]
+        except KeyError:
+            return default
 
-        Parameters
-        ----------
-        argv : list, optional
-            A list that has the form of sys.argv[1:] which has unicode
-            elements of the form u"key=value". If this is None (default),
-            then self.argv will be used.
-        aliases : dict
-            A dict of aliases for configurable traits.
-            Keys are the short aliases, Values are the resolved trait.
-            Of the form: `{'alias' : 'Configurable.trait'}`
-        flags : dict
-            A dict of flags, keyed by str name. Values can be Config objects
-            or dicts.  When the flag is triggered, The config is loaded as
-            `self.config.update(cfg)`.
-        """
-        self.clear()
-        if argv is None:
-            argv = self.argv
-        if aliases is None:
-            aliases = self.aliases
-        if flags is None:
-            flags = self.flags
-
-        # ensure argv is a list of unicode strings:
-        uargv = self._decode_argv(argv)
-        for idx, raw in enumerate(uargv):
-            if raw == '--':
-                # Don't parse arguments after '--'.
-                # This is useful for relaying arguments to scripts, e.g.:
-                #     ipython -i foo.py --matplotlib=qt -- args after '--' go-to-foo.py
-                self.extra_args.extend(uargv[idx + 1:])
-                break
-
-            # strip leading '-'
-            item = raw.lstrip('-')
-
-            if not item:
-                ## It was either a lone '-' (i.e. denoting STDIN),
-                #  or more than 3 '---'; append it as is.
-                self.extra_args.append(raw)
-                continue
 
-            if kv_pattern.match(raw):
-                lhs, rhs = item.split('=', 1)
-                # Substitute longnames for aliases.
-                if lhs in aliases:
-                    lhs = aliases[lhs]
-                if '.' not in lhs:
-                    # probably a mistyped alias, but not technically illegal
-                    self.log.warning("Unrecognized alias: '%s', it will probably have no effect.", raw)
-                try:
-                    self._exec_config_str(lhs, rhs)
-                except Exception:
-                    raise ArgumentError("Invalid argument: '%s'" % raw)
-
-            elif flag_pattern.match(raw):
-                if item in flags:
-                    cfg, _ = flags[item]
-                    self._load_flag(cfg)
-                else:
-                    raise ArgumentError("Unrecognized flag: '%s'" % raw)
-            elif raw.startswith('-'):
-                kv = '--' + item
-                if kv_pattern.match(kv):
-                    raise ArgumentError("Invalid argument: '%s', did you mean '%s'?" % (raw, kv))
-                else:
-                    raise ArgumentError("Invalid argument: '%s'" % raw)
-            else:
-                # keep all args that aren't valid in a list,
-                # in case our parent knows what to do with them.
-                self.extra_args.append(item)
-        return self.config
+class _KVArgParser(argparse.ArgumentParser):
+    """subclass of ArgumentParser where any --Class.trait option is implicitly defined"""
+    def parse_known_args(self, args=None, namespace=None):
+        # must be done immediately prior to parsing because if we do it in init,
+        # registration of explicit actions via parser.add_option will fail during setup
+        for container in (self, self._optionals):
+            container._option_string_actions = _DefaultOptionDict(
+                container._option_string_actions)
+        return super().parse_known_args(args, namespace)
 
 
 class ArgParseConfigLoader(CommandLineConfigLoader):
     """A loader that uses the argparse module to load from the command line."""
 
+    parser_class = ArgumentParser
+
     def __init__(self, argv=None, aliases=None, flags=None, log=None, classes=(),
                  *parser_args, **parser_kw):
         """Create a config loader for use with argparse.
@@ -817,19 +797,36 @@ class ArgParseConfigLoader(CommandLineConfigLoader):
         kwargs.update(parser_kw)
         self.parser_kw = kwargs
 
-    def load_config(self, argv=None, aliases=None, flags=None, classes=None):
+    def load_config(self, argv=None, aliases=None, flags=_deprecated, classes=None):
         """Parse command line arguments and return as a Config object.
 
         Parameters
         ----------
-        args : optional, list
+        argv : optional, list
             If given, a list with the structure of sys.argv[1:] to parse
             arguments from. If not given, the instance's self.argv attribute
-            (given at construction time) is used."""
+            (given at construction time) is used.
+        flags
+            Deprecated in traitlets 5.0, instanciate the config loader with the flags.
+
+        """
+
+        if flags is not _deprecated:
+            warnings.warn(
+                "The `flag` argument to load_config is deprecated since Traitlets "
+                f"5.0 and will be ignored, pass flags the `{type(self)}` constructor.",
+                DeprecationWarning,
+                stacklevel=2,
+            )
+
         self.clear()
         if argv is None:
             argv = self.argv
-        self._create_parser(aliases, flags, classes)
+        if aliases is not None:
+            self.aliases = aliases
+        if classes is not None:
+            self.classes = classes
+        self._create_parser()
         self._parse_args(argv)
         self._convert_to_config()
         return self.config
@@ -840,16 +837,11 @@ class ArgParseConfigLoader(CommandLineConfigLoader):
         else:
             return []
 
-    def _create_parser(self, aliases=None, flags=None, classes=None):
-        self.parser = ArgumentParser(*self.parser_args, **self.parser_kw)
-        self._add_arguments(aliases, flags, classes)
+    def _create_parser(self):
+        self.parser = self.parser_class(*self.parser_args, **self.parser_kw)
+        self._add_arguments(self.aliases, self.flags, self.classes)
 
-    def _parse_config_traits(self):
-        for cls in self.classes:
-            for trait, traitname in cls.class_traits(config=True).items():
-                yield ()
-
-    def _add_arguments(self, aliases=None, flags=None, classes=None):
+    def _add_arguments(self, aliases, flags, classes):
         raise NotImplementedError("subclasses must implement _add_arguments")
 
     def _parse_args(self, args):
@@ -857,30 +849,54 @@ class ArgParseConfigLoader(CommandLineConfigLoader):
         # decode sys.argv to support unicode command-line options
         enc = DEFAULT_ENCODING
         uargs = [cast_unicode(a, enc) for a in args]
-        self.parsed_data, self.extra_args = self.parser.parse_known_args(uargs)
+
+        unpacked_aliases = {}
+        if self.aliases:
+            unpacked_aliases = {}
+            for alias, alias_target in self.aliases.items():
+                if alias in self.flags:
+                    continue
+                if not isinstance(alias, tuple):
+                    short_alias, alias = alias, None
+                else:
+                    short_alias, alias = alias
+                for al in (short_alias, alias):
+                    if al is None:
+                        continue
+                    if len(al) == 1:
+                        unpacked_aliases["-" + al] = "--" + alias_target
+                    unpacked_aliases["--" + al] = "--" + alias_target
+
+        def _replace(arg):
+            if arg == "-":
+                return _DASH_REPLACEMENT
+            for k, v in unpacked_aliases.items():
+                if arg == k:
+                    return v
+                if arg.startswith(k + "="):
+                    return v + "=" + arg[len(k) + 1:]
+            return arg
+
+        if '--' in uargs:
+            idx = uargs.index('--')
+            extra_args = uargs[idx+1:]
+            to_parse = uargs[:idx]
+        else:
+            extra_args = []
+            to_parse = uargs
+        to_parse = [_replace(a) for a in to_parse]
+
+        self.parsed_data = self.parser.parse_args(to_parse)
+        self.extra_args = extra_args
 
     def _convert_to_config(self):
         """self.parsed_data->self.config"""
         for k, v in vars(self.parsed_data).items():
-            exec("self.config.%s = v"%k, locals(), globals())
-
-## For Dict traits, describes the cmd-line option as `key=value`
-_kv_opt_pattern = re.compile(r'^([^=]+)=(.*)$')
-
-def _kv_opt(traitname, opt_value):
-    """
-    Used as `type` when adding args into :meth:`ArgumentParser.add_argument()`
-    corresponding to config Dict-traits.
-    """
-    if opt_value[0] == '{' and opt_value[-1] == '}' and ':' in opt_value:
-        # leave opt_value as a string for parsing later
-        return opt_value
-
-    m = _kv_opt_pattern.match(opt_value)
-    if not m:
-        raise ArgumentError("Expecting <key>=<value> for Dict-trait '%s', got %r!"
-                            % (traitname, opt_value))
-    return m.groups()
+            *path, key = k.split(".")
+            section = self.config
+            for p in path:
+                section = section[p]
+            setattr(section, key, v)
 
 
 class _FlagAction(argparse.Action):
@@ -894,7 +910,6 @@ class _FlagAction(argparse.Action):
         super(_FlagAction, self).__init__(*args, **kwargs)
 
     def __call__(self, parser, namespace, values, option_string=None):
-        key = option_string.lstrip('-')
         if self.nargs == 0 or values is Undefined:
             namespace._flags.append(self.flag)
         else:
@@ -907,17 +922,13 @@ class KVArgParseConfigLoader(ArgParseConfigLoader):
     of common args, such as `ipython -c 'print 5'`, but still gets
     arbitrary config with `ipython --InteractiveShell.autoindent=False`"""
 
-    def _add_arguments(self, aliases=None, flags=None, classes=None):
+    parser_class = _KVArgParser
+
+    def _add_arguments(self, aliases, flags, classes):
         alias_flags = {}
-        # print aliases, flags
-        if aliases is None:
-            aliases = self.aliases
-        if flags is None:
-            flags = self.flags
-        if classes is None:
-            classes = self.classes
         paa = self.parser.add_argument
         self.parser.set_defaults(_flags=[])
+        paa("extra_args", nargs="*")
 
         ## An index of all container traits collected::
         #
@@ -936,26 +947,28 @@ class KVArgParseConfigLoader(ArgParseConfigLoader):
                         argparse_kwds['action'] = multiplicity
                     else:
                         argparse_kwds['nargs'] = multiplicity
-                    if isinstance(trait, Dict):
-                        argparse_kwds['type'] = fnt.partial(_kv_opt, traitname)
                 argparse_traits[argname] = (trait, argparse_kwds)
-                paa('--'+argname, **argparse_kwds)
 
         for keys, (value, _) in flags.items():
             if not isinstance(keys, tuple):
-                keys = (keys, )
+                keys = (keys,)
             for key in keys:
-                if key in self.aliases:
-                    alias_flags[self.aliases[key]] = value
+                if key in aliases:
+                    alias_flags[aliases[key]] = value
                     continue
-                keys = ('-'+key, '--'+key) if len(key) == 1 else ('--'+key, )
+                keys = ('-' + key, '--' + key) if len(key) == 1 else ('--' + key,)
                 paa(*keys, action=_FlagAction, flag=value)
 
         for keys, traitname in aliases.items():
             if not isinstance(keys, tuple):
-                keys = (keys, )
+                keys = (keys,)
+
             for key in keys:
-                argparse_kwds = {'type': str, 'dest': traitname}
+                argparse_kwds = {
+                    'type': str,
+                    'dest': traitname.replace(".", _DOT_REPLACEMENT),
+                    'metavar': traitname,
+                }
                 if traitname in argparse_traits:
                     argparse_kwds.update(argparse_traits[traitname][1])
                     if 'action' in argparse_kwds and traitname in alias_flags:
@@ -973,29 +986,59 @@ class KVArgParseConfigLoader(ArgParseConfigLoader):
                     argparse_kwds['action'] = _FlagAction
                     argparse_kwds['flag'] = alias_flags[traitname]
                     argparse_kwds['alias'] = traitname
-                keys = ('-'+key, '--'+key) if len(key) == 1 else ('--'+key, )
+                keys = ('-' + key, '--' + key) if len(key) == 1 else ('--'+ key,)
                 paa(*keys, **argparse_kwds)
 
     def _convert_to_config(self):
         """self.parsed_data->self.config, parse unrecognized extra args via KVLoader."""
-        for k, v in vars(self.parsed_data).items():
-            if k == '_flags':
+        extra_args = self.extra_args
+
+        for lhs, rhs in vars(self.parsed_data).items():
+            if lhs == "extra_args":
+                self.extra_args = ["-" if a == _DASH_REPLACEMENT else a for a in rhs] + extra_args
+                continue
+            elif lhs == '_flags':
                 # _flags will be handled later
                 continue
-            trait = self.argparse_traits.get(k)
+
+            lhs = lhs.replace(_DOT_REPLACEMENT, ".")
+            if '.' not in lhs:
+                # probably a mistyped alias, but not technically illegal
+                self.log.warning("Unrecognized alias: '%s', it will have no effect.", lhs)
+                trait = None
+
+            if isinstance(rhs, list):
+                rhs = DeferredConfigList(rhs)
+            elif isinstance(rhs, str):
+                rhs = DeferredConfigString(rhs)
+
+            trait = self.argparse_traits.get(lhs)
             if trait:
                 trait = trait[0]
+
             # eval the KV assignment
-            self._exec_config_str(k, v, trait=trait)
+            try:
+                self._exec_config_str(lhs, rhs, trait)
+            except Exception:
+                raise ArgumentError("Invalid argument: '%s=%s'" % (lhs, rhs))
 
         for subc in self.parsed_data._flags:
             self._load_flag(subc)
 
-        if self.extra_args:
-            sub_parser = KeyValueConfigLoader(log=self.log)
-            sub_parser.load_config(self.extra_args)
-            self.config.merge(sub_parser.config)
-            self.extra_args = sub_parser.extra_args
+
+class KeyValueConfigLoader(KVArgParseConfigLoader):
+    """Deprecated in traitlets 5.0
+
+    Use KVArgParseConfigLoader
+    """
+    def __init__(self, *args, **kwargs):
+        warnings.warn(
+            "KeyValueConfigLoader is deprecated since Traitlets 5.0."
+            " Use KVArgParseConfigLoader instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        super().__init__(*args, **kwargs)
 
 
 def load_pyconfig_files(config_files, path):
diff --git a/traitlets/traitlets.py b/traitlets/traitlets.py
index 49b9ce6..c53b934 100644
--- a/traitlets/traitlets.py
+++ b/traitlets/traitlets.py
@@ -39,6 +39,7 @@ Inheritance diagram:
 # Adapted from enthought.traits, Copyright (c) Enthought, Inc.,
 # also under the terms of the Modified BSD License.
 
+from ast import literal_eval
 import contextlib
 import inspect
 import os
@@ -160,6 +161,17 @@ def _deprecated_method(method, cls, method_name, msg):
     else:
         warn_explicit(warn_msg, DeprecationWarning, fname, lineno)
 
+def _safe_literal_eval(s):
+    """Safely evaluate an expression
+
+    Returns original string if eval fails.
+
+    Use only where types are ambiguous.
+    """
+    try:
+        return literal_eval(s)
+    except (NameError, SyntaxError, ValueError):
+        return s
 
 def is_trait(t):
     """ Returns whether the given value is an instance or subclass of TraitType.
@@ -468,6 +480,20 @@ class TraitType(BaseDescriptor):
         if help is not None:
             self.metadata['help'] = help
 
+    def from_string(self, s):
+        """Get a value from a config string
+
+        such as an environment variable or CLI arguments.
+
+        Traits can override this method to define their own
+        parsing of config strings.
+
+        .. seealso:: item_from_string
+
+        .. versionadded:: 5.0
+        """
+        return s
+
     def default(self, obj=None):
         """The default generator for this trait
 
@@ -609,13 +635,13 @@ class TraitType(BaseDescriptor):
 
         Parameters
         ----------
-        obj: HasTraits or None
+        obj : HasTraits or None
             The instance which owns the trait. If not
             object is given, then an object agnostic
             error will be raised.
-        value: any
+        value : any
             The value that caused the error.
-        error: Exception (default: None)
+        error : Exception (default: None)
             An error that was raised by a child trait.
             The arguments of this exception should be
             of the form ``(value, info, *traits)``.
@@ -625,7 +651,7 @@ class TraitType(BaseDescriptor):
             of :class:`TraitType` instances that are
             "children" of this one (the first being
             the deepest).
-        info: str (default: None)
+        info : str (default: None)
             A description of the expected value. By
             default this is infered from this trait's
             ``info`` method.
@@ -799,8 +825,6 @@ class MetaHasTraits(MetaHasDescriptors):
         super(MetaHasTraits, cls).setup_class(classdict)
 
 
-
-
 def observe(*names, **kwargs):
     """A decorator which can be used to observe Traits on a class.
 
@@ -820,7 +844,7 @@ def observe(*names, **kwargs):
     ----------
     *names
         The str names of the Traits to observe on the object.
-    type: str, kwarg-only
+    type : str, kwarg-only
         The type of event to observe (e.g. 'change')
     """
     if not names:
@@ -1622,7 +1646,7 @@ class HasTraits(HasDescriptors, metaclass=MetaHasTraits):
 
         Parameters
         ----------
-        name: str (default: None)
+        name : str (default: None)
             The name of a trait of this class. If name is ``None`` then all
             the event handlers of this class will be returned instead.
 
@@ -1842,6 +1866,9 @@ class Instance(ClassBasedTraitType):
     def default_value_repr(self):
         return repr(self.make_dynamic_default())
 
+    def from_string(self, s):
+        return _safe_literal_eval(s)
+
 
 class ForwardDeclaredMixin(object):
     """
@@ -1905,7 +1932,7 @@ class Union(TraitType):
 
         Parameters
         ----------
-        trait_types: sequence
+        trait_types : sequence
             The list of trait types of length at least 1.
 
         Notes
@@ -2009,6 +2036,9 @@ class Int(TraitType):
             self.error(obj, value)
         return _validate_bounds(self, obj, value)
 
+    def from_string(self, s):
+        return int(s)
+
 
 class CInt(Int):
     """A casting version of the int trait."""
@@ -2044,6 +2074,9 @@ class Float(TraitType):
             self.error(obj, value)
         return _validate_bounds(self, obj, value)
 
+    def from_string(self, s):
+        return float(s)
+
 
 class CFloat(Float):
     """A casting version of the float trait."""
@@ -2069,6 +2102,9 @@ class Complex(TraitType):
             return complex(value)
         self.error(obj, value)
 
+    def from_string(self, s):
+        return complex(s)
+
 
 class CComplex(Complex):
     """A casting version of the complex number trait."""
@@ -2093,6 +2129,9 @@ class Bytes(TraitType):
             return value
         self.error(obj, value)
 
+    def from_string(self, s):
+        return s.encode('utf8')
+
 
 class CBytes(Bytes):
     """A casting version of the byte string trait."""
@@ -2121,6 +2160,21 @@ class Unicode(TraitType):
                 raise TraitError(msg.format(value, self.name, class_of(obj)))
         self.error(obj, value)
 
+    def from_string(self, s):
+        s = os.path.expanduser(s)
+        if len(s) >= 2:
+            # handle deprecated "1"
+            for c in ('"', "'"):
+                if s[0] == s[-1] == c:
+                    old_s = s
+                    s = s[1:-1]
+                    warn(
+                        "Supporting extra quotes around Unicode is deprecated in traitlets 5.0. "
+                        "Use %r instead of %r – or use CUnicode." % (s, old_s),
+                        FutureWarning)
+        return s
+
+
 
 class CUnicode(Unicode):
     """A casting version of the unicode trait."""
@@ -2147,6 +2201,9 @@ class ObjectName(TraitType):
             return value
         self.error(obj, value)
 
+    def from_string(self, s):
+        return s
+
 class DottedObjectName(ObjectName):
     """A string holding a valid dotted object name in Python, such as A.b3._c"""
     def validate(self, obj, value):
@@ -2167,8 +2224,22 @@ class Bool(TraitType):
     def validate(self, obj, value):
         if isinstance(value, bool):
             return value
+        elif isinstance(value, int):
+            if value == 1:
+                return True
+            elif value == 0:
+                return False
         self.error(obj, value)
 
+    def from_string(self, s):
+        s = s.lower()
+        if s in {'true', '1'}:
+            return True
+        elif s in {'false', '0'}:
+            return False
+        else:
+            raise ValueError("%r is not 1, 0, true, or false")
+
 
 class CBool(Bool):
     """A casting version of the boolean trait."""
@@ -2216,6 +2287,12 @@ class Enum(TraitType):
     def info_rst(self):
         return self._info(as_rst=True)
 
+    def from_string(self, s):
+        try:
+            return self.validate(None, s)
+        except TraitError:
+            return _safe_literal_eval(s)
+
 
 class CaselessStrEnum(Enum):
     """An enum of strings where the case should be ignored."""
@@ -2355,7 +2432,7 @@ class Container(Instance):
         elif trait is not None:
             raise TypeError("`trait` must be a Trait or None, got %s" % repr_type(trait))
 
-        super(Container,self).__init__(klass=self.klass, args=args, **kwargs)
+        super(Container, self).__init__(klass=self.klass, args=args, **kwargs)
 
     def validate(self, obj, value):
         if isinstance(value, self._cast_types):
@@ -2391,6 +2468,47 @@ class Container(Instance):
             self._trait.instance_init(obj)
         super(Container, self).instance_init(obj)
 
+    def from_string(self, s):
+        """Load value from a single string"""
+        if not isinstance(s, str):
+            raise TraitError(f"Expected string, got {s!r}")
+        try:
+            test = literal_eval(s)
+        except Exception:
+            test = None
+        return self.validate(None, test)
+
+    def from_string_list(self, s_list):
+        """Return the value from a list of config strings
+
+        This is where we parse CLI configuration
+        """
+        if len(s_list) == 1:
+            # check for deprecated --Class.trait="['a', 'b', 'c']"
+            r = s_list[0]
+            if (
+                (r[0] == '[' and r[-1] == ']') or
+                (r[0] == '(' and r[-1] == ')')
+            ):
+                warn(
+                    "--{0}={1} for containers is deprecated in traitlets 5.0. "
+                    "You can pass --{0} item ... multiple times to add items to a list.".format(
+                        self.name, r),
+                    FutureWarning
+                )
+                return self.klass(literal_eval(r))
+        return self.klass([self.item_from_string(s) for s in s_list])
+
+    def item_from_string(self, s):
+        """Cast a single item from a string
+
+        Evaluated when parsing CLIconfiguration from a string
+        """
+        if self._trait:
+            return self._trait.from_string(s)
+        else:
+            return s
+
 
 class List(Container):
     """An instance of a Python list."""
@@ -2754,6 +2872,72 @@ class Dict(Instance):
         super(Dict, self).instance_init(obj)
 
 
+    def from_string(self, s):
+        """Load value from a single string"""
+        if not isinstance(s, str):
+            raise TypeError(f"from_string expects a list, got {repr(s)} of type {type(s)}")
+        try:
+            return self.from_string_list([s])
+        except Exception:
+            test = _safe_literal_eval(s)
+            if isinstance(test, dict):
+                return test
+            raise
+
+    def from_string_list(self, s_list):
+        """Return the value from a list of config strings
+
+        This is where we parse CLI configuration
+        """
+        if (
+            len(s_list) == 1
+            and s_list[0].startswith("{")
+            and s_list[0].endswith("}")
+        ):
+            warn(
+                "--{0}={1} for dict-traits is deprecated in traitlets 5.0. "
+                "You can pass --{0} <key=value> ... multiple times to add items to a dict.".format(
+                    self.name,
+                    s_list[0],
+                ),
+                FutureWarning,
+            )
+
+            return literal_eval(s_list[0])
+
+        combined = {}
+        for d in [self.item_from_string(s) for s in s_list]:
+            combined.update(d)
+        return combined
+
+    def item_from_string(self, s):
+        """Cast a single item from a string
+
+        Evaluated when parsing CLIconfiguration from a string
+
+        Dicts expect strings of the form key=value
+
+        Returns a one-key dictionary
+        """
+
+        if '=' not in s:
+            raise TraitError(
+                "'%s' options must have the form 'key=value', got %s"
+                % (self.__class__.__name__, repr(s),)
+            )
+        key, value = s.split("=", 1)
+
+        # cast key with key trait, if defined
+        if self._key_trait:
+            key = self._key_trait.from_string(key)
+
+        # cast value with value trait, if defined (per-key or global)
+        value_trait = (self._per_key_traits or {}).get(key, self._value_trait)
+        if value_trait:
+            value = value_trait.from_string(value)
+        return {key: value}
+
+
 class TCPAddress(TraitType):
     """A trait for an (ip, port) tuple.
 
@@ -2772,6 +2956,14 @@ class TCPAddress(TraitType):
                         return value
         self.error(obj, value)
 
+    def from_string(self, s):
+        if ':' not in s:
+            raise ValueError('Require `ip:port`, got %r' % s)
+        ip, port = s.split(':', 1)
+        port = int(port)
+        return (ip, port)
+
+
 class CRegExp(TraitType):
     """A casting compiled regular expression trait.
 
@@ -2886,6 +3078,7 @@ class UseEnum(TraitType):
         return self._info(as_rst=True)
 
 
+
 class Callable(TraitType):
     """A trait which is callable.
 
@@ -2902,6 +3095,7 @@ class Callable(TraitType):
         else:
             self.error(obj, value)
 
+
 def _add_all():
     """add all trait types to `__all__`
 
diff --git a/traitlets/utils/descriptions.py b/traitlets/utils/descriptions.py
index e3bd917..5c30422 100644
--- a/traitlets/utils/descriptions.py
+++ b/traitlets/utils/descriptions.py
@@ -8,7 +8,7 @@ def describe(article, value, name=None, verbose=False, capital=False):
 
     Parameters
     ----------
-    article: str or None
+    article : str or None
         A definite or indefinite article. If the article is
         indefinite (i.e. "a" or "an") the appropriate one
         will be infered. Thus, the arguments of ``describe``
@@ -17,18 +17,18 @@ def describe(article, value, name=None, verbose=False, capital=False):
         will be prepended to the result. For non-articled
         description, values that are instances are treated
         definitely, while classes are handled indefinitely.
-    value: any
+    value : any
         The value which will be named.
-    name: str or None (default: None)
+    name : str or None (default: None)
         Only applies when ``article`` is "the" - this
         ``name`` is a definite reference to the value.
         By default one will be infered from the value's
         type and repr methods.
-    verbose: bool (default: False)
+    verbose : bool (default: False)
         Whether the name should be concise or verbose. When
         possible, verbose names include the module, and/or
         class name where an object was defined.
-    capital: bool (default: False)
+    capital : bool (default: False)
         Whether the first letter of the article should
         be capitalized or not. By default it is not.
 
@@ -137,11 +137,11 @@ def add_article(name, definite=False, capital=False):
 
     Parameters
     ----------
-    definite: bool (default: False)
+    definite : bool (default: False)
         Whether the article is definite or not.
         Indefinite articles being 'a' and 'an',
         while 'the' is definite.
-    capital: bool (default: False)
+    capital : bool (default: False)
         Whether the added article should have
         its first letter capitalized or not.
     """


Test patch:
diff --git a/traitlets/config/tests/test_application.py b/traitlets/config/tests/test_application.py
index 0a3d25e..b757975 100644
--- a/traitlets/config/tests/test_application.py
+++ b/traitlets/config/tests/test_application.py
@@ -14,16 +14,29 @@ import os
 import sys
 from io import StringIO
 from tempfile import TemporaryDirectory
-from unittest import TestCase, skip
+from unittest import TestCase
 
 from pytest import mark
 
-from traitlets import Bool, Dict, HasTraits, Integer, List, Set, Tuple, Unicode
+from traitlets import (
+    Bool,
+    Bytes,
+    Dict,
+    HasTraits,
+    Integer,
+    List,
+    Set,
+    Tuple,
+    Unicode,
+)
 from traitlets.config.application import Application
 from traitlets.config.configurable import Configurable
 from traitlets.config.loader import Config
-from traitlets.tests.utils import (check_help_all_output, check_help_output,
-                                   get_output_error_code)
+from traitlets.tests.utils import (
+    check_help_all_output,
+    check_help_output,
+    get_output_error_code,
+)
 
 try:
     from unittest import mock
@@ -33,9 +46,6 @@ except ImportError:
 pjoin = os.path.join
 
 
-
-
-
 class Foo(Configurable):
 
     i = Integer(0, help="""
@@ -46,8 +56,10 @@ class Foo(Configurable):
     j = Integer(1, help="The integer j.").tag(config=True)
     name = Unicode(u'Brian', help="First name.").tag(config=True)
     la = List([]).tag(config=True)
+    li = List(Integer()).tag(config=True)
     fdict = Dict().tag(config=True, multiplicity='+')
 
+
 class Bar(Configurable):
 
     b = Integer(0, help="The integer b.").tag(config=True)
@@ -55,6 +67,8 @@ class Bar(Configurable):
     tb = Tuple(()).tag(config=True, multiplicity='*')
     aset = Set().tag(config=True, multiplicity='+')
     bdict = Dict().tag(config=True)
+    idict = Dict(value_trait=Integer()).tag(config=True)
+    key_dict = Dict(per_key_traits={'i': Integer(), 'b': Bytes()}).tag(config=True)
 
 
 class MyApp(Application):
@@ -74,6 +88,7 @@ class MyApp(Application):
                     ('j', 'fooj') : ('Foo.j', "`j` terse help msg"),
                     'name' : 'Foo.name',
                     'la': 'Foo.la',
+                    'li': 'Foo.li',
                     'tb': 'Bar.tb',
                     'D': 'Bar.bdict',
                     'enabled' : 'Bar.enabled',
@@ -106,7 +121,6 @@ def class_to_names(classes):
 
 
 class TestApplication(TestCase):
-
     def test_log(self):
         stream = StringIO()
         app = MyApp(log_level=logging.INFO)
@@ -118,6 +132,12 @@ class TestApplication(TestCase):
         app.log.info("hello")
         assert "hello" in stream.getvalue()
 
+    def test_no_eval_cli_text(self):
+        app = MyApp()
+        app.initialize(['--Foo.name=1'])
+        app.init_foo()
+        assert app.foo.name == '1'
+
     def test_basic(self):
         app = MyApp()
         self.assertEqual(app.name, u'myapp')
@@ -156,40 +176,53 @@ class TestApplication(TestCase):
 
     def test_config(self):
         app = MyApp()
-        app.parse_command_line(["--i=10","--Foo.j=10","--enable=False","--log-level=50"])
+        app.parse_command_line([
+            "--i=10",
+            "--Foo.j=10",
+            "--enable=False",
+            "--log-level=50",
+        ])
         config = app.config
+        print(config)
         self.assertEqual(config.Foo.i, 10)
         self.assertEqual(config.Foo.j, 10)
         self.assertEqual(config.Bar.enabled, False)
-        self.assertEqual(config.MyApp.log_level,50)
+        self.assertEqual(config.MyApp.log_level, 50)
 
     def test_config_seq_args(self):
         app = MyApp()
-        app.parse_command_line("--la 1 --tb AB 2 --Foo.la=ab --Bar.aset S1 S2 S1".split())
+        app.parse_command_line("--li 1 --li 3 --la 1 --tb AB 2 --Foo.la=ab --Bar.aset S1 --Bar.aset S2 --Bar.aset S1".split())
+        assert app.extra_args == ["2"]
         config = app.config
-        self.assertEqual(config.Foo.la, [1, 'ab'])
-        self.assertEqual(config.Bar.tb, ['AB', 2])
-        self.assertEqual(config.Bar.aset, 'S1 S2 S1'.split())
+        assert config.Foo.li == [1, 3]
+        assert config.Foo.la == ["1", "ab"]
+        assert config.Bar.tb == ("AB",)
+        self.assertEqual(config.Bar.aset, {"S1", "S2"})
         app.init_foo()
-        self.assertEqual(app.foo.la, [1, 'ab'])
+        assert app.foo.li == [1, 3]
+        assert app.foo.la == ['1', 'ab']
         app.init_bar()
         self.assertEqual(app.bar.aset, {'S1', 'S2'})
-        self.assertEqual(app.bar.tb, ('AB', 2))
+        assert app.bar.tb == ('AB',)
 
     def test_config_dict_args(self):
         app = MyApp()
         app.parse_command_line(
-            "--Foo.fdict a=1 b=b c=3 "
+            "--Foo.fdict a=1 --Foo.fdict b=b --Foo.fdict c=3 "
             "--Bar.bdict k=1 -D=a=b -D 22=33 "
+            "--Bar.idict k=1 --Bar.idict b=2 --Bar.idict c=3 "
             .split())
-        fdict = {'a': 1, 'b': 'b', 'c': 3}
-        bdict = {'k': 1, 'a': 'b', '22': 33}
+        fdict = {'a': '1', 'b': 'b', 'c': '3'}
+        bdict = {'k': '1', 'a': 'b', '22': '33'}
+        idict = {'k': 1, 'b': 2, 'c': 3}
         config = app.config
+        assert config.Bar.idict == idict
         self.assertDictEqual(config.Foo.fdict, fdict)
         self.assertDictEqual(config.Bar.bdict, bdict)
         app.init_foo()
         self.assertEqual(app.foo.fdict, fdict)
         app.init_bar()
+        assert app.bar.idict == idict
         self.assertEqual(app.bar.bdict, bdict)
 
     def test_config_propagation(self):
@@ -380,7 +413,7 @@ class TestApplication(TestCase):
     def test_extra_args(self):
 
         app = MyApp()
-        app.parse_command_line(["--Bar.b=5", 'extra', "--disable", 'args'])
+        app.parse_command_line(["--Bar.b=5", 'extra', 'args', "--disable"])
         app.init_bar()
         self.assertEqual(app.bar.enabled, False)
         self.assertEqual(app.bar.b, 5)
@@ -394,8 +427,10 @@ class TestApplication(TestCase):
         self.assertEqual(app.extra_args, ['extra', '--disable', 'args'])
 
         app = MyApp()
-        app.parse_command_line(['-', '--disable', '--Bar.b=1', '-', 'extra'])
-        self.assertEqual(app.extra_args, ['-', '-', 'extra'])
+        app.parse_command_line(
+            ["--disable", "--la", "-", "-", "--Bar.b=1", "--", "-", "extra"]
+        )
+        self.assertEqual(app.extra_args, ["-", "-", "extra"])
 
     def test_unicode_argv(self):
         app = MyApp()
@@ -554,7 +589,7 @@ class TestApplication(TestCase):
 
             app.load_config_file(name, path=[td1])
             self.assertEqual(len(app.loaded_config_files), 1)
-            self.assertEquals(app.loaded_config_files[0], config_file)
+            self.assertEqual(app.loaded_config_files[0], config_file)
 
             app.start()
             self.assertEqual(app.running, True)
diff --git a/traitlets/config/tests/test_configurable.py b/traitlets/config/tests/test_configurable.py
index 07228cb..35f42f5 100644
--- a/traitlets/config/tests/test_configurable.py
+++ b/traitlets/config/tests/test_configurable.py
@@ -4,8 +4,6 @@
 # Distributed under the terms of the Modified BSD License.
 
 import logging
-import sys
-import warnings
 from unittest import TestCase
 
 from pytest import mark
@@ -31,7 +29,7 @@ class MyConfigurable(Configurable):
     c = Unicode('no config')
 
 
-mc_help=u"""MyConfigurable(Configurable) options
+mc_help = u"""MyConfigurable(Configurable) options
 ------------------------------------
 --MyConfigurable.a=<Integer>
     The integer a.
@@ -426,26 +424,24 @@ class TestParentConfigurable(TestCase):
 
     def test_multi_parent_priority(self):
         cfg = Config({
-            'MyConfigurable' : {
-                'b' : 2.0,
+            'MyConfigurable': {
+                'b': 2.0,
             },
-            'MyParent' : {
-                'MyConfigurable' : {
-                    'b' : 3.0,
-                }
+            'MyParent': {
+                'MyConfigurable': {
+                    'b': 3.0,
+                },
             },
-            'MyParent2' : {
-                'MyConfigurable' : {
-                    'b' : 4.0,
-                }
+            'MyParent2': {
+                'MyConfigurable': {
+                    'b': 4.0,
+                },
+                'MyParent': {
+                    'MyConfigurable': {
+                        'b': 5.0,
+                    },
+                },
             },
-            'MyParent2' : {
-                'MyParent' : {
-                    'MyConfigurable' : {
-                        'b' : 5.0,
-                    }
-                }
-            }
         })
         parent2 = MyParent2(config=cfg)
         parent = MyParent2(parent=parent2)
diff --git a/traitlets/config/tests/test_loader.py b/traitlets/config/tests/test_loader.py
index 4263fb6..5813a69 100644
--- a/traitlets/config/tests/test_loader.py
+++ b/traitlets/config/tests/test_loader.py
@@ -7,11 +7,11 @@
 import copy
 import os
 import pickle
-import sys
+from itertools import chain
 from tempfile import mkstemp
 from unittest import TestCase
 
-from pytest import skip
+import pytest
 
 from traitlets.config.loader import (
     Config,
@@ -21,9 +21,14 @@ from traitlets.config.loader import (
     KeyValueConfigLoader,
     ArgParseConfigLoader,
     KVArgParseConfigLoader,
-    ConfigError,
 )
-from traitlets import (HasTraits, Union, List, Tuple, Dict, Int, Unicode)
+from traitlets import (
+    List,
+    Tuple,
+    Dict,
+    Unicode,
+    Integer,
+)
 from traitlets.config import Configurable
 
 
@@ -68,8 +73,8 @@ import logging
 log = logging.getLogger('devnull')
 log.setLevel(0)
 
-class TestFileCL(TestCase):
 
+class TestFileCL(TestCase):
     def _check_conf(self, config):
         self.assertEqual(config.a, 10)
         self.assertEqual(config.b, 20)
@@ -173,12 +178,14 @@ class TestFileCL(TestCase):
         with self.assertRaises(ValueError):
             cl.load_config()
 
+
 def _parse_int_or_str(v):
     try:
         return int(v)
     except:
         return str(v)
 
+
 class MyLoader1(ArgParseConfigLoader):
     def _add_arguments(self, aliases=None, flags=None, classes=None):
         p = self.parser
@@ -189,14 +196,16 @@ class MyLoader1(ArgParseConfigLoader):
         p.add_argument('--list1', action='append', type=_parse_int_or_str)
         p.add_argument('--list2', nargs='+', type=int)
 
+
 class MyLoader2(ArgParseConfigLoader):
     def _add_arguments(self, aliases=None, flags=None, classes=None):
         subparsers = self.parser.add_subparsers(dest='subparser_name')
         subparser1 = subparsers.add_parser('1')
-        subparser1.add_argument('-x',dest='Global.x')
+        subparser1.add_argument('-x', dest='Global.x')
         subparser2 = subparsers.add_parser('2')
         subparser2.add_argument('y')
 
+
 class TestArgParseCL(TestCase):
 
     def test_basic(self):
@@ -237,94 +246,153 @@ class TestArgParseCL(TestCase):
         self.assertEqual(config.list2, [1, 2, 3])
 
 
+class C(Configurable):
+    str_trait = Unicode(config=True)
+    int_trait = Integer(config=True)
+    list_trait = List(config=True)
+    list_of_ints = List(Integer(), config=True)
+    dict_trait = Dict(config=True)
+    dict_of_ints = Dict(
+        key_trait=Integer(),
+        value_trait=Integer(),
+        config=True,
+    )
+    dict_multi = Dict(
+        key_trait=Unicode(),
+        per_key_traits={
+            "int": Integer(),
+            "str": Unicode(),
+        },
+        config=True,
+    )
+
+
 class TestKeyValueCL(TestCase):
     klass = KeyValueConfigLoader
 
     def test_eval(self):
         cl = self.klass(log=log)
-        config = cl.load_config('--Class.str_trait=all --Class.int_trait=5 --Class.list_trait=["hello",5]'.split())
-        self.assertEqual(config.Class.str_trait, 'all')
-        self.assertEqual(config.Class.int_trait, 5)
-        self.assertEqual(config.Class.list_trait, ["hello", 5])
+        config = cl.load_config('--C.str_trait=all --C.int_trait=5 --C.list_trait=["hello",5]'.split())
+        c = C(config=config)
+        assert c.str_trait == 'all'
+        assert c.int_trait == 5
+        assert c.list_trait == ["hello", 5]
 
     def test_basic(self):
         cl = self.klass(log=log)
         argv = [ '--' + s[2:] for s in pyfile.split('\n') if s.startswith('c.') ]
-        print(argv)
         config = cl.load_config(argv)
-        self.assertEqual(config.a, 10)
-        self.assertEqual(config.b, 20)
-        self.assertEqual(config.Foo.Bar.value, 10)
+        assert config.a == '10'
+        assert config.b == '20'
+        assert config.Foo.Bar.value == '10'
         # non-literal expressions are not evaluated
         self.assertEqual(config.Foo.Bam.value, 'list(range(10))')
-        self.assertEqual(config.D.C.value, 'hi there')
+        self.assertEqual(Unicode().from_string(config.D.C.value), 'hi there')
 
     def test_expanduser(self):
         cl = self.klass(log=log)
         argv = ['--a=~/1/2/3', '--b=~', '--c=~/', '--d="~/"']
         config = cl.load_config(argv)
-        self.assertEqual(config.a, os.path.expanduser('~/1/2/3'))
-        self.assertEqual(config.b, os.path.expanduser('~'))
-        self.assertEqual(config.c, os.path.expanduser('~/'))
-        self.assertEqual(config.d, '~/')
+        u = Unicode()
+        self.assertEqual(u.from_string(config.a), os.path.expanduser('~/1/2/3'))
+        self.assertEqual(u.from_string(config.b), os.path.expanduser('~'))
+        self.assertEqual(u.from_string(config.c), os.path.expanduser('~/'))
+        self.assertEqual(u.from_string(config.d), '~/')
 
     def test_extra_args(self):
         cl = self.klass(log=log)
-        config = cl.load_config(['--a=5', 'b', '--c=10', 'd'])
+        config = cl.load_config(['--a=5', 'b', 'd', '--c=10'])
         self.assertEqual(cl.extra_args, ['b', 'd'])
-        self.assertEqual(config.a, 5)
-        self.assertEqual(config.c, 10)
+        assert config.a == '5'
+        assert config.c == '10'
         config = cl.load_config(['--', '--a=5', '--c=10'])
         self.assertEqual(cl.extra_args, ['--a=5', '--c=10'])
 
         cl = self.klass(log=log)
-        config = cl.load_config(['-', 'extra', '--a=2', '--c=1', '-'])
-        self.assertEqual(cl.extra_args, ['-', 'extra', '-'])
+        config = cl.load_config(['extra', '--a=2', '--c=1', '--', '-'])
+        self.assertEqual(cl.extra_args, ['extra', '-'])
 
     def test_unicode_args(self):
         cl = self.klass(log=log)
         argv = [u'--a=épsîlön']
         config = cl.load_config(argv)
+        print(config, cl.extra_args)
         self.assertEqual(config.a, u'épsîlön')
 
-    def test_unicode_bytes_args(self):
-        uarg = u'--a=é'
-        try:
-            barg = uarg.encode(sys.stdin.encoding)
-        except (TypeError, UnicodeEncodeError):
-            raise skip("sys.stdin.encoding can't handle 'é'")
+    def test_list_append(self):
+        cl = self.klass(log=log)
+        argv = ["--C.list_trait", "x", "--C.list_trait", "y"]
+        config = cl.load_config(argv)
+        assert config.C.list_trait == ["x", "y"]
+        c = C(config=config)
+        assert c.list_trait == ["x", "y"]
 
+    def test_dict(self):
         cl = self.klass(log=log)
-        config = cl.load_config([barg])
-        self.assertEqual(config.a, u'é')
+        argv = ["--C.dict_trait", "x=5", "--C.dict_trait", "y=10"]
+        config = cl.load_config(argv)
+        c = C(config=config)
+        assert c.dict_trait == {"x": "5", "y": "10"}
 
-    def test_unicode_alias(self):
+    def test_dict_key_traits(self):
         cl = self.klass(log=log)
-        argv = [u'--a=épsîlön']
-        config = cl.load_config(argv, aliases=dict(a='A.a'))
-        self.assertEqual(config.A.a, u'épsîlön')
+        argv = ["--C.dict_of_ints", "1=2", "--C.dict_of_ints", "3=4"]
+        config = cl.load_config(argv)
+        c = C(config=config)
+        assert c.dict_of_ints == {1: 2, 3: 4}
 
 
-class CBase(HasTraits):
+class CBase(Configurable):
     a = List().tag(config=True)
-    b = List().tag(config=True, multiplicity='*')
+    b = List(Integer()).tag(config=True, multiplicity='*')
     c = List().tag(config=True, multiplicity='append')
     adict = Dict().tag(config=True)
 
+
 class CSub(CBase):
     d = Tuple().tag(config=True)
     e = Tuple().tag(config=True, multiplicity='+')
     bdict = Dict().tag(config=True, multiplicity='*')
 
+
 class TestArgParseKVCL(TestKeyValueCL):
     klass = KVArgParseConfigLoader
 
+    def test_no_cast_literals(self):
+        cl = self.klass(log=log)
+        # test ipython -c 1 doesn't cast to int
+        argv = ["-c", "1"]
+        config = cl.load_config(argv, aliases=dict(c="IPython.command_to_run"))
+        assert config.IPython.command_to_run == "1"
+
+    def test_int_literals(self):
+        cl = self.klass(log=log)
+        # test ipython -c 1 doesn't cast to int
+        argv = ["-c", "1"]
+        config = cl.load_config(argv, aliases=dict(c="IPython.command_to_run"))
+        assert config.IPython.command_to_run == "1"
+
+    def test_unicode_alias(self):
+        cl = self.klass(log=log)
+        argv = [u'--a=épsîlön']
+        config = cl.load_config(argv, aliases=dict(a='A.a'))
+        print(dict(config))
+        print(cl.extra_args)
+        print(cl.aliases)
+        self.assertEqual(config.A.a, u'épsîlön')
+
     def test_expanduser2(self):
         cl = self.klass(log=log)
         argv = ['-a', '~/1/2/3', '--b', "'~/1/2/3'"]
         config = cl.load_config(argv, aliases=dict(a='A.a', b='A.b'))
-        self.assertEqual(config.A.a, os.path.expanduser('~/1/2/3'))
-        self.assertEqual(config.A.b, '~/1/2/3')
+
+        class A(Configurable):
+            a = Unicode(config=True)
+            b = Unicode(config=True)
+
+        a = A(config=config)
+        self.assertEqual(a.a, os.path.expanduser('~/1/2/3'))
+        self.assertEqual(a.b, '~/1/2/3')
 
     def test_eval(self):
         cl = self.klass(log=log)
@@ -335,15 +403,16 @@ class TestArgParseKVCL(TestKeyValueCL):
     def test_seq_traits(self):
         cl = self.klass(log=log, classes=(CBase, CSub))
         aliases = {'a3': 'CBase.c', 'a5': 'CSub.e'}
-        argv = ("--CBase.a A --CBase.a 2 --CBase.b 1 2 3 --a3 AA --CBase.c BB "
-                "--CSub.d 1 --CSub.d BBB --CSub.e 1 a bcd").split()
+        argv = ("--CBase.a A --CBase.a 2 --CBase.b 1 --CBase.b 3 --a3 AA --CBase.c BB "
+                "--CSub.d 1 --CSub.d BBB --CSub.e 1 --CSub.e=bcd a b c ").split()
         config = cl.load_config(argv, aliases=aliases)
-        self.assertEqual(config.CBase.a, ['A', 2])
-        self.assertEqual(config.CBase.b, [1, 2, 3])
+        assert cl.extra_args == ["a", "b", "c"]
+        assert config.CBase.a == ['A', '2']
+        assert config.CBase.b == [1, 3]
         self.assertEqual(config.CBase.c, ['AA', 'BB'])
 
-        self.assertEqual(config.CSub.d, [1, 'BBB'])
-        self.assertEqual(config.CSub.e, [1, 'a', 'bcd'])
+        assert config.CSub.d == ('1', 'BBB')
+        assert config.CSub.e == ('1', 'bcd')
 
     def test_seq_traits_single_empty_string(self):
         cl = self.klass(log=log, classes=(CBase, ))
@@ -355,12 +424,33 @@ class TestArgParseKVCL(TestKeyValueCL):
     def test_dict_traits(self):
         cl = self.klass(log=log, classes=(CBase, CSub))
         aliases = {'D': 'CBase.adict', 'E': 'CSub.bdict'}
-        argv = ["--CBase.adict", "k1=v1", "-D=k2=2", "-D", "k3=v 3", "-E", "k=v", "22=222"]
+        argv = ["-D", "k1=v1", "-D=k2=2", "-D", "k3=v 3", "-E", "k=v", "-E", "22=222"]
         config = cl.load_config(argv, aliases=aliases)
-        self.assertDictEqual(config.CBase.adict,
-                {'k1': 'v1', 'k2': 2, 'k3': 'v 3'})
-        self.assertEqual(config.CSub.bdict,
-                {'k': 'v', '22': 222})
+        c = CSub(config=config)
+        assert c.adict == {'k1': 'v1', 'k2': '2', 'k3': 'v 3'}
+        assert c.bdict == {'k': 'v', '22': '222'}
+
+    def test_mixed_seq_positional(self):
+        aliases = {"c": "Class.trait"}
+        cl = self.klass(log=log, aliases=aliases)
+        assignments = [("-c", "1"), ("--Class.trait=2",), ("--c=3",), ("--Class.trait", "4")]
+        positionals = ["a", "b", "c"]
+        # test with positionals at any index
+        for idx in range(len(assignments) + 1):
+            argv_parts = assignments[:]
+            argv_parts[idx:idx] = (positionals,)
+            argv = list(chain(*argv_parts))
+
+            config = cl.load_config(argv)
+            assert config.Class.trait == ["1", "2", "3", "4"]
+            assert cl.extra_args == ["a", "b", "c"]
+
+    def test_split_positional(self):
+        """Splitting positionals across flags is no longer allowed in traitlets 5"""
+        cl = self.klass(log=log)
+        argv = ["a", "--Class.trait=5", "b"]
+        with pytest.raises(SystemExit):
+            cl.load_config(argv)
 
 
 class TestConfig(TestCase):
diff --git a/traitlets/tests/_warnings.py b/traitlets/tests/_warnings.py
index c3ce685..05e9168 100644
--- a/traitlets/tests/_warnings.py
+++ b/traitlets/tests/_warnings.py
@@ -95,7 +95,7 @@ def expected_warnings(matching):
         # enter context
         yield w
         # exited user context, check the recorded warnings
-        remaining = [m for m in matching if not '\A\Z' in m.split('|')]
+        remaining = [m for m in matching if not r'\A\Z' in m.split('|')]
         for warn in w:
             found = False
             for match in matching:
diff --git a/traitlets/tests/test_traitlets.py b/traitlets/tests/test_traitlets.py
index bd813f3..d178d82 100644
--- a/traitlets/tests/test_traitlets.py
+++ b/traitlets/tests/test_traitlets.py
@@ -17,12 +17,49 @@ import pytest
 from pytest import mark
 
 from traitlets import (
-    HasTraits, MetaHasTraits, TraitType, Any, Bool, CBytes, Dict, Enum,
-    Int, CInt, Long, CLong, Integer, Float, CFloat, Complex, Bytes, Unicode,
-    TraitError, Union, Callable, All, Undefined, Type, This, Instance, TCPAddress,
-    List, Tuple, ObjectName, DottedObjectName, CRegExp, link, directional_link,
-    ForwardDeclaredType, ForwardDeclaredInstance, validate, observe, default,
-    observe_compat, BaseDescriptor, HasDescriptors,
+    HasTraits,
+    MetaHasTraits,
+    TraitType,
+    Any,
+    Bool,
+    CBytes,
+    Dict,
+    Enum,
+    Int,
+    CInt,
+    Long,
+    CLong,
+    Integer,
+    Float,
+    CFloat,
+    Complex,
+    Bytes,
+    Unicode,
+    TraitError,
+    Union,
+    Callable,
+    All,
+    Undefined,
+    Type,
+    This,
+    Instance,
+    TCPAddress,
+    List,
+    Tuple,
+    ObjectName,
+    DottedObjectName,
+    CRegExp,
+    link,
+    directional_link,
+    ForwardDeclaredType,
+    ForwardDeclaredInstance,
+    validate,
+    observe,
+    default,
+    observe_compat,
+    BaseDescriptor,
+    HasDescriptors,
+    CUnicode,
 )
 
 def change_dict(*ordered_values):
@@ -102,7 +139,7 @@ class TestTraitType(TestCase):
 
     def test_error(self):
         class A(HasTraits):
-            tt = TraitType
+            tt = TraitType()
         a = A()
         self.assertRaises(TraitError, A.tt.error, a, 10)
 
@@ -2596,6 +2633,7 @@ def test_override_default_instance():
     c._a_default = lambda self: 'overridden'
     assert c.a == 'overridden'
 
+
 def test_copy_HasTraits():
     from copy import copy
 
@@ -2609,3 +2647,129 @@ def test_copy_HasTraits():
     cc.a = 2
     assert cc.a == 2
     assert c.a == 1
+
+
+def _from_string_test(traittype, s, expected):
+    """Run a test of trait.from_string"""
+    if isinstance(traittype, TraitType):
+        trait = traittype
+    else:
+        trait = traittype()
+    if isinstance(s, list):
+        cast = trait.from_string_list
+    else:
+        cast = trait.from_string
+    if type(expected) is type and issubclass(expected, Exception):
+        with pytest.raises(expected):
+            value = cast(s)
+            trait.validate(None, value)
+    else:
+        value = cast(s)
+        assert value == expected
+
+
+@pytest.mark.parametrize(
+    "s, expected", [("xyz", "xyz"), ("1", "1"), ('"xx"', "xx"), ("'abc'", "abc"),]
+)
+def test_unicode_from_string(s, expected):
+    _from_string_test(Unicode, s, expected)
+
+
+@pytest.mark.parametrize(
+    "s, expected", [("xyz", "xyz"), ("1", "1"), ('"xx"', "xx"), ("'abc'", "abc"),]
+)
+def test_cunicode_from_string(s, expected):
+    _from_string_test(CUnicode, s, expected)
+
+
+@pytest.mark.parametrize('s, expected', [
+    ('x', ValueError),
+    ('1', 1),
+    ('123', 123),
+    ('2.0', ValueError),
+])
+def test_int_from_string(s, expected):
+    _from_string_test(Integer, s, expected)
+
+
+@pytest.mark.parametrize('s, expected', [
+    ('x', ValueError),
+    ('1', 1.0),
+    ('123.5', 123.5),
+    ('2.5', 2.5),
+])
+def test_float_from_string(s, expected):
+    _from_string_test(Float, s, expected)
+
+
+@pytest.mark.parametrize('s, expected', [
+    ('x', ValueError),
+    ('1', 1.0),
+    ('123.5', 123.5),
+    ('2.5', 2.5),
+    ('1+2j', 1+2j),
+])
+def test_complex_from_string(s, expected):
+    _from_string_test(Complex, s, expected)
+
+
+@pytest.mark.parametrize('s, expected', [
+    ('true', True),
+    ('TRUE', True),
+    ('1', True),
+    ('0', False),
+    ('False', False),
+    ('false', False),
+    ('1.0', ValueError),
+])
+def test_bool_from_string(s, expected):
+    _from_string_test(Bool, s, expected)
+
+
+@pytest.mark.parametrize('s, expected', [
+    ('{}', {}),
+    ('1', TraitError),
+    ('{1: 2}', {1: 2}),
+    ('{"key": "value"}', {"key": "value"}),
+    ('x', TraitError),
+])
+def test_dict_from_string(s, expected):
+    _from_string_test(Dict, s, expected)
+
+
+@pytest.mark.parametrize('s, expected', [
+    ('[]', []),
+    ('[1, 2, "x"]', [1, 2, 'x']),
+    (["1", "x"], ["1", "x"])
+])
+def test_list_from_string(s, expected):
+    _from_string_test(List, s, expected)
+
+
+@pytest.mark.parametrize('s, expected, value_trait', [
+    (["1", "2", "3"], [1, 2, 3], Integer()),
+    (["x"], ValueError, Integer()),
+    (["1", "x"], ["1", "x"], Unicode())
+])
+def test_list_from_string(s, expected, value_trait):
+    _from_string_test(List(value_trait), s, expected)
+
+
+@pytest.mark.parametrize('s, expected', [
+    ('x', 'x'),
+    ('mod.submod', 'mod.submod'),
+    ('not an identifier', TraitError),
+    ('1', '1'),
+])
+def test_object_from_string(s, expected):
+    _from_string_test(DottedObjectName, s, expected)
+
+
+@pytest.mark.parametrize('s, expected', [
+    ('127.0.0.1:8000', ('127.0.0.1', 8000)),
+    ('host.tld:80', ('host.tld', 80)),
+    ('host:notaport', ValueError),
+    ('127.0.0.1', ValueError),
+])
+def test_tcp_from_string(s, expected):
+    _from_string_test(TCPAddress, s, expected)
diff --git a/traitlets/utils/tests/test_importstring.py b/traitlets/utils/tests/test_importstring.py
index b3734f7..6202ff8 100644
--- a/traitlets/utils/tests/test_importstring.py
+++ b/traitlets/utils/tests/test_importstring.py
@@ -25,5 +25,5 @@ class TestImportItem(TestCase):
             "import_item accepts strings, "
             "not '%s'." % NotAString
         )
-        with self.assertRaisesRegexp(TypeError, msg):
+        with self.assertRaisesRegex(TypeError, msg):
             import_item(NotAString())