Repository: theacodes/nox (python)

Issue:
Re-factor how Nox documents and stores command line flags
As brought up in #145 (and #147), it's cumbersome to manage the flags and their documentation. There are several locations where new flags have to be updated:

1. `usage.rst`.
2. `__main__.py` (several spots).
3. `test_main.py`
4. `test_sessions.py` (usually).

Preferably, we'd have one source of truth for the name, type, and description of these args. We'd use this source of truth to generate the rest.

Pull request description:
Refactor how Nox defines and process options
Closes #146, although there will need to be a follow up to use this information in the documentation.

Patch:
diff --git a/nox/__init__.py b/nox/__init__.py
index cc41c82..d83dbeb 100644
--- a/nox/__init__.py
+++ b/nox/__init__.py
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from nox._options import options
+from nox._options import noxfile_options as options
 from nox._parametrize import Param as param
 from nox._parametrize import parametrize_decorator as parametrize
 from nox.registry import session_decorator as session
diff --git a/nox/__main__.py b/nox/__main__.py
index 093d0f7..65f589f 100644
--- a/nox/__main__.py
+++ b/nox/__main__.py
@@ -19,222 +19,21 @@ function). This module primarily loads configuration, and then passes
 control to :meth:``nox.workflow.execute``.
 """
 
-import argparse
-import os
 import sys
 
 import pkg_resources
 
+from nox import _options
 from nox import tasks
 from nox import workflow
 from nox.logger import setup_logging
 
 
-def _default_with_off_flag(current, default, off_flag):
-    """Helper method for merging command line args and noxfile config.
-
-    Returns False if off_flag is set, otherwise, returns the default value if
-    set, otherwise, returns the current value.
-    """
-    return (default or current) and not off_flag
-
-
-class GlobalConfig:
-    def __init__(self, args):
-        self.noxfile = args.noxfile
-        self.envdir = args.envdir
-        self.sessions = args.sessions
-        self.keywords = args.keywords
-        self.list_sessions = args.list_sessions
-        self.reuse_existing_virtualenvs = args.reuse_existing_virtualenvs
-        self.no_reuse_existing_virtualenvs = args.no_reuse_existing_virtualenvs
-        self.stop_on_first_error = args.stop_on_first_error
-        self.no_stop_on_first_error = args.no_stop_on_first_error
-        self.error_on_missing_interpreters = args.error_on_missing_interpreters
-        self.no_error_on_missing_interpreters = args.no_error_on_missing_interpreters
-        self.error_on_external_run = args.error_on_external_run
-        self.no_error_on_external_run = args.no_error_on_external_run
-        self.install_only = args.install_only
-        self.posargs = args.posargs
-        self.report = args.report
-        self.color = not args.nocolor or args.forcecolor
-
-        if self.posargs and self.posargs[0] == "--":
-            self.posargs.pop(0)
-
-    def merge_from_options(self, options):
-        """Update the config from the Noxfile-specified options.
-
-        The options function as "defaults" for the most part, with some small
-        caveats documented in the body of the function.
-
-        Args:
-            options (nox._options.options): The options set in the Noxfile.
-        """
-        # If *either* sessions or keywords are specified on the command line,
-        # ignore *both* sessions and keywords in Noxfile.
-        if not self.sessions and not self.keywords:
-            self.sessions = options.sessions
-            self.keywords = options.keywords
-
-        self.envdir = self.envdir or options.envdir or ".nox"
-        self.reuse_existing_virtualenvs = _default_with_off_flag(
-            self.reuse_existing_virtualenvs,
-            options.reuse_existing_virtualenvs,
-            self.no_reuse_existing_virtualenvs,
-        )
-        self.stop_on_first_error = _default_with_off_flag(
-            self.stop_on_first_error,
-            options.stop_on_first_error,
-            self.no_stop_on_first_error,
-        )
-        self.error_on_missing_interpreters = _default_with_off_flag(
-            self.error_on_missing_interpreters,
-            options.error_on_missing_interpreters,
-            self.no_error_on_missing_interpreters,
-        )
-        self.error_on_external_run = _default_with_off_flag(
-            self.error_on_external_run,
-            options.error_on_external_run,
-            self.no_error_on_external_run,
-        )
-        self.report = self.report or options.report
-
-
 def main():
-    parser = argparse.ArgumentParser(
-        description="Nox is a Python automation toolkit.", add_help=False
-    )
-    primary = parser.add_argument_group(
-        "Primary arguments",
-        "These are the most common arguments used when invoking Nox.",
-    )
-
-    primary.add_argument(
-        "-h", "--help", action="store_true", help="Show this help message and exit."
-    )
-
-    primary.add_argument(
-        "--version", action="store_true", help="Show the Nox version and exit."
-    )
-
-    primary.add_argument(
-        "-l",
-        "--list-sessions",
-        action="store_true",
-        help="List all available sessions and exit.",
-    )
-
-    primary.add_argument(
-        "-s",
-        "-e",
-        "--sessions",
-        nargs="*",
-        default=_get_default_sessions(),
-        help="Which sessions to run, by default, all sessions will run.",
-    )
-
-    primary.add_argument(
-        "-k", "--keywords", help="Only run sessions that match the given expression."
-    )
-
-    primary.add_argument(
-        "posargs",
-        nargs=argparse.REMAINDER,
-        help="Arguments following -- that are passed through to the session(s).",
-    )
-
-    secondary = parser.add_argument_group(
-        "Additional arguments & flags",
-        "These arguments are used to control Nox's behavior or control advanced features.",
-    )
-
-    secondary.add_argument(
-        "-r",
-        "--reuse-existing-virtualenvs",
-        action="store_true",
-        help="Re-use existing virtualenvs instead of recreating them.",
-    )
-    secondary.add_argument(
-        "--no-reuse-existing-virtualenvs",
-        action="store_true",
-        help="Disables --reuse-existing-virtualenvs if it is enabled in the Noxfile.",
-    )
-
-    secondary.add_argument(
-        "-f",
-        "--noxfile",
-        default="noxfile.py",
-        help="Location of the Python file containing nox sessions.",
-    )
-
-    secondary.add_argument(
-        "--envdir",
-        help="Directory where nox will store virtualenvs, this is .nox by default.",
-    )
-
-    secondary.add_argument(
-        "-x",
-        "--stop-on-first-error",
-        action="store_true",
-        help="Stop after the first error.",
-    )
-    secondary.add_argument(
-        "--no-stop-on-first-error",
-        action="store_true",
-        help="Disables --stop-on-first-error if it is enabled in the Noxfile.",
-    )
-
-    secondary.add_argument(
-        "--error-on-missing-interpreters",
-        action="store_true",
-        help="Error instead of skip if an interpreter can not be located.",
-    )
-    secondary.add_argument(
-        "--no-error-on-missing-interpreters",
-        action="store_true",
-        help="Disables --error-on-missing-interpreters if it is enabled in the Noxfile.",
-    )
-
-    secondary.add_argument(
-        "--error-on-external-run",
-        action="store_true",
-        help="Error if run() is used to execute a program that isn't installed in a session's virtualenv.",
-    )
-    secondary.add_argument(
-        "--no-error-on-external-run",
-        action="store_true",
-        help="Disables --error-on-external-run if it is enabled in the Noxfile.",
-    )
-
-    secondary.add_argument(
-        "--install-only",
-        action="store_true",
-        help="Skip session.run invocations in the Noxfile.",
-    )
-
-    secondary.add_argument(
-        "--report", help="Output a report of all sessions to the given filename."
-    )
-
-    secondary.add_argument(
-        "--nocolor",
-        default="NO_COLOR" in os.environ or not sys.stderr.isatty(),
-        action="store_true",
-        help="Disable all color output.",
-    )
-
-    secondary.add_argument(
-        "--forcecolor",
-        default=False,
-        action="store_true",
-        help="Force color output, even if stdout is not an interactive terminal.",
-    )
-
-    args = parser.parse_args()
+    args = _options.options.parse_args()
 
     if args.help:
-        parser.print_help()
+        _options.options.print_help()
         return
 
     if args.version:
@@ -242,12 +41,11 @@ def main():
         print(dist.version, file=sys.stderr)
         return
 
-    global_config = GlobalConfig(args)
-    setup_logging(color=global_config.color)
+    setup_logging(color=args.color)
 
     # Execute the appropriate tasks.
     exit_code = workflow.execute(
-        global_config=global_config,
+        global_config=args,
         workflow=(
             tasks.load_nox_module,
             tasks.merge_noxfile_options,
@@ -266,11 +64,5 @@ def main():
     sys.exit(exit_code)
 
 
-def _get_default_sessions():
-    nox_env = os.environ.get("NOXSESSION")
-    env_sessions = nox_env.split(",") if nox_env else None
-    return env_sessions
-
-
 if __name__ == "__main__":  # pragma: no cover
     main()
diff --git a/nox/_option_set.py b/nox/_option_set.py
new file mode 100644
index 0000000..4425b7d
--- /dev/null
+++ b/nox/_option_set.py
@@ -0,0 +1,286 @@
+# Copyright 2019 Alethea Katherine Flowers
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""High-level options interface. This allows defining options just once that
+can be specified from the command line and the noxfile, easily used in tests,
+and surfaced in documentation."""
+
+
+import argparse
+import collections
+import functools
+
+
+Namespace = argparse.Namespace
+ArgumentError = argparse.ArgumentError
+
+
+class Option:
+    """A single option that can be specified via command-line or configuration
+    file.
+
+    Args:
+        name (str): The name used to refer to the option in the final namespace
+            object.
+        flags (Sequence[str]): The list of flags used by argparse. Effectively
+            the ``*args`` for ``ArgumentParser.add_argument``.
+        help (str): The help string pass to argparse.
+        group (str): The argument group this option belongs to, if any.
+        noxfile (str): Whether or not this option can be set in the
+            configuration file.
+        merge_func (Callable[[Namespace, Namespace], Any]): A function that
+            can define custom behavior when merging the command-line options
+            with the configuration file options. The first argument is the
+            command-line options, the second is the configuration file options.
+            It should return the new value for the option.
+        finalizer_func (Callable[Any, Namespace], Any): A function that can
+            define custom finalization behavior. This is called after all
+            arguments are parsed. It's called with the options parsed value
+            and the set of command-line options and should return the new
+            value.
+        default (Union[Any, Callable[[], Any]]): The default value. It may
+            also be a function in which case it will be invoked after argument
+            parsing if nothing was specified.
+        hidden (bool): Means this option will be present in the namespace, but
+            will not show up on the argument list.
+        kwargs: Passed through to``ArgumentParser.add_argument``.
+    """
+
+    def __init__(
+        self,
+        name,
+        *flags,
+        help=None,
+        group=None,
+        noxfile=False,
+        merge_func=None,
+        finalizer_func=None,
+        default=None,
+        hidden=False,
+        **kwargs
+    ):
+        self.name = name
+        self.flags = flags
+        self.help = help
+        self.group = group
+        self.noxfile = noxfile
+        self.merge_func = merge_func
+        self.finalizer_func = finalizer_func
+        self.hidden = hidden
+        self.kwargs = kwargs
+        self._default = default
+
+    @property
+    def default(self):
+        if callable(self._default):
+            return self._default()
+        return self._default
+
+
+def flag_pair_merge_func(enable_name, disable_name, command_args, noxfile_args):
+    """Merge function for flag pairs. If the flag is set in the noxfile or
+    the command line params, return ``True`` *unless* the disable flag has been
+    specified on the command-line.
+
+    For example, assuming you have a flag pair created using::
+
+        make_flag_pair(
+            "thing_a",
+            "--thing-a",
+            "--no-thing-a"
+        )
+
+    Then if the Noxfile says::
+
+        nox.options.thing_a = True
+
+    But the command line says::
+
+        nox --no-thing-a
+
+    Then the result will be ``False``.
+
+    However, without the "--no-thing-a" flag set then this returns ``True`` if
+    *either*::
+
+        nox.options.thing_a = True
+
+    or::
+
+        nox --thing-a
+
+    are specified.
+    """
+    noxfile_value = getattr(noxfile_args, enable_name)
+    command_value = getattr(command_args, enable_name)
+    disable_value = getattr(command_args, disable_name)
+
+    return (command_value or noxfile_value) and not disable_value
+
+
+def make_flag_pair(name, enable_flags, disable_flags, **kwargs):
+    """Returns two options - one to enable a behavior and another to disable it.
+
+    The positive option is considered to be available to the noxfile, as
+    there isn't much point in doing flag pairs without it.
+    """
+    disable_name = "no_{}".format(name)
+
+    kwargs["action"] = "store_true"
+    enable_option = Option(
+        name,
+        *enable_flags,
+        noxfile=True,
+        merge_func=functools.partial(flag_pair_merge_func, name, disable_name),
+        **kwargs
+    )
+
+    kwargs["help"] = "Disables {} if it is enabled in the Noxfile.".format(
+        enable_flags[-1]
+    )
+    disable_option = Option(disable_name, *disable_flags, **kwargs)
+
+    return enable_option, disable_option
+
+
+class OptionSet:
+    """A set of options.
+
+    A high-level wrapper over ``argparse.ArgumentParser``. It allows for
+    introspection of options as well as quality-of-life features such as
+    finalization, callable defaults, and strongly typed namespaces for tests.
+    """
+
+    def __init__(self, *args, **kwargs):
+        self.parser_args = args
+        self.parser_kwargs = kwargs
+        self.options = collections.OrderedDict()
+        self.groups = collections.OrderedDict()
+
+    def add_options(self, *args):
+        """Adds a sequence of Options to the OptionSet.
+
+        Args:
+            args (Sequence[Options])
+        """
+        for option in args:
+            self.options[option.name] = option
+
+    def add_group(self, name, *args, **kwargs):
+        """Adds a new argument group.
+
+        When :func:`parser` is invoked, the OptionSet will turn all distinct
+        argument groups into separate sections in the ``--help`` output using
+        ``ArgumentParser.add_argument_group``.
+        """
+        self.groups[name] = (args, kwargs)
+
+    def _add_to_parser(self, parser, option):
+        parser.add_argument(
+            *option.flags, help=option.help, default=option.default, **option.kwargs
+        )
+
+    def parser(self):
+        """Returns an ``ArgumentParser`` for this option set.
+
+        Generally, you won't use this directly. Instead, use
+        :func:`parse_args`.
+        """
+        parser = argparse.ArgumentParser(*self.parser_args, **self.parser_kwargs)
+
+        groups = {
+            name: parser.add_argument_group(*args, **kwargs)
+            for name, (args, kwargs) in self.groups.items()
+        }
+
+        for option in self.options.values():
+            if option.hidden:
+                continue
+
+            if option.group is not None:
+                self._add_to_parser(groups[option.group], option)
+            else:
+                self._add_to_parser(parser, option)
+
+        return parser
+
+    def print_help(self):
+        return self.parser().print_help()
+
+    def _finalize_args(self, args):
+        """Does any necessary post-processing on arguments."""
+        for option in self.options.values():
+            # Handle hidden items.
+            if option.hidden and not hasattr(args, option.name):
+                setattr(args, option.name, option.default)
+
+            value = getattr(args, option.name)
+
+            # Handle options that have finalizer functions.
+            if option.finalizer_func:
+                setattr(args, option.name, option.finalizer_func(value, args))
+
+    def parse_args(self):
+        parser = self.parser()
+        args = parser.parse_args()
+
+        try:
+            self._finalize_args(args)
+        except ArgumentError as err:
+            parser.error(str(err))
+        return args
+
+    def namespace(self, **kwargs):
+        """Return a namespace that contains all of the options in this set.
+
+        kwargs can be used to set values and does so in a checked way - you
+        can not set an option that does not exist in the set. This is useful
+        for testing.
+        """
+        args = {option.name: option.default for option in self.options.values()}
+
+        # Don't use update - validate that the keys actually exist so that
+        # we don't accidentally set non-existent options.
+        # don't bother with coverage here, this is effectively only ever
+        # used in tests.
+        for key, value in kwargs.items():
+            if key not in args:
+                raise KeyError("{} is not an option.".format(key))
+            args[key] = value
+
+        return argparse.Namespace(**args)
+
+    def noxfile_namespace(self):
+        """Returns a namespace of options that can be set in the configuration
+        file."""
+        return argparse.Namespace(
+            **{
+                option.name: option.default
+                for option in self.options.values()
+                if option.noxfile
+            }
+        )
+
+    def merge_namespaces(self, command_args, noxfile_args):
+        """Merges the command-line options with the noxfile options."""
+        for name, option in self.options.items():
+            if option.merge_func:
+                setattr(
+                    command_args, name, option.merge_func(command_args, noxfile_args)
+                )
+            elif option.noxfile:
+                value = getattr(command_args, name, None) or getattr(
+                    noxfile_args, name, None
+                )
+                setattr(command_args, name, value)
diff --git a/nox/_options.py b/nox/_options.py
index bef4fc3..4dcadda 100644
--- a/nox/_options.py
+++ b/nox/_options.py
@@ -12,23 +12,240 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import argparse
+import functools
+import os
+import sys
 
-class options:
-    """Options that are configurable in the Noxfile.
+from nox import _option_set
 
-    By setting properties on ``nox.options`` you can specify command line
-    arguments in your Noxfile. If an argument is specified in both the Noxfile
-    and on the command line, the command line arguments take precedence.
+"""All of nox's configuration options."""
 
-    See :doc:`usage` for more details on these settings and their effect.
+options = _option_set.OptionSet(
+    description="Nox is a Python automation toolkit.", add_help=False
+)
+
+options.add_group(
+    "primary",
+    "Primary arguments",
+    "These are the most common arguments used when invoking Nox.",
+)
+options.add_group(
+    "secondary",
+    "Additional arguments & flags",
+    "These arguments are used to control Nox's behavior or control advanced features.",
+)
+
+
+def _sessions_and_keywords_merge_func(key, command_args, noxfile_args):
+    """Only return the Noxfile value for sessions/keywords if neither sessions
+    or keywords are specified on the command-line.
+
+    Args:
+        key (str): This function is used for both the "sessions" and "keywords"
+            options, this allows using ``funtools.partial`` to pass the
+            same function for both options.
+        command_args (_option_set.Namespace): The options specified on the
+            command-line.
+        noxfile_Args (_option_set.Namespace): The options specified in the
+            Noxfile."""
+    if not command_args.sessions and not command_args.keywords:
+        return getattr(noxfile_args, key)
+    return getattr(command_args, key)
+
+
+def _envdir_merge_func(command_args, noxfile_args):
+    """Ensure that there is always some envdir.
+
+    Args:
+        command_args (_option_set.Namespace): The options specified on the
+            command-line.
+        noxfile_Args (_option_set.Namespace): The options specified in the
+            Noxfile.
+    """
+    return command_args.envdir or noxfile_args.envdir or ".nox"
+
+
+def _sessions_default():
+    """Looks at the NOXSESSION env var to set the default value for sessions."""
+    nox_env = os.environ.get("NOXSESSION")
+    env_sessions = nox_env.split(",") if nox_env else None
+    return env_sessions
+
+
+def _color_finalizer(value, args):
+    """Figures out the correct value for "color" based on the two color flags.
+
+    Args:
+        value (bool): The current value of the "color" option.
+        args (_option_set.Namespace): The values for all options.
+
+    Returns:
+        The new value for the "color" option.
     """
+    if args.forcecolor is True and args.nocolor is True:
+        raise _option_set.ArgumentError(
+            None, "Can not specify both --no-color and --force-color."
+        )
+
+    if args.forcecolor is True:
+        return True
+
+    if args.nocolor is True:
+        return False
+
+    return sys.stderr.isatty()
+
+
+def _posargs_finalizer(value, unused_args):
+    """Removes any leading "--"s in the posargs array."""
+    if value and value[0] == "--":
+        value.pop(0)
+    return value
+
+
+options.add_options(
+    _option_set.Option(
+        "help",
+        "-h",
+        "--help",
+        group="primary",
+        action="store_true",
+        help="Show this help message and exit.",
+    ),
+    _option_set.Option(
+        "version",
+        "--version",
+        group="primary",
+        action="store_true",
+        help="Show the Nox version and exit.",
+    ),
+    _option_set.Option(
+        "list_sessions",
+        "-l",
+        "--list-sessions",
+        "--list",
+        group="primary",
+        action="store_true",
+        help="List all available sessions and exit.",
+    ),
+    _option_set.Option(
+        "sessions",
+        "-s",
+        "-e",
+        "--sessions",
+        "--session",
+        group="primary",
+        noxfile=True,
+        merge_func=functools.partial(_sessions_and_keywords_merge_func, "sessions"),
+        nargs="*",
+        default=_sessions_default,
+        help="Which sessions to run. By default, all sessions will run.",
+    ),
+    _option_set.Option(
+        "keywords",
+        "-k",
+        "--keywords",
+        noxfile=True,
+        merge_func=functools.partial(_sessions_and_keywords_merge_func, "keywords"),
+        help="Only run sessions that match the given expression.",
+    ),
+    _option_set.Option(
+        "posargs",
+        "posargs",
+        group="primary",
+        nargs=argparse.REMAINDER,
+        help="Arguments following ``--`` that are passed through to the session(s).",
+        finalizer_func=_posargs_finalizer,
+    ),
+    *_option_set.make_flag_pair(
+        "reuse_existing_virtualenvs",
+        ("-r", "--reuse-existing-virtualenvs"),
+        ("--no-reuse-existing-virtualenvs",),
+        group="secondary",
+        help="Re-use existing virtualenvs instead of recreating them.",
+    ),
+    _option_set.Option(
+        "noxfile",
+        "-f",
+        "--noxfile",
+        group="secondary",
+        default="noxfile.py",
+        help="Location of the Python file containing nox sessions.",
+    ),
+    _option_set.Option(
+        "envdir",
+        "--envdir",
+        noxfile=True,
+        merge_func=_envdir_merge_func,
+        group="secondary",
+        help="Directory where nox will store virtualenvs, this is ``.nox`` by default.",
+    ),
+    *_option_set.make_flag_pair(
+        "stop_on_first_error",
+        ("-x", "--stop-on-first-error"),
+        ("--no-stop-on-first-error",),
+        group="secondary",
+        help="Stop after the first error.",
+    ),
+    *_option_set.make_flag_pair(
+        "error_on_missing_interpreters",
+        ("--error-on-missing-interpreters",),
+        ("--no-error-on-missing-interpreters",),
+        group="secondary",
+        help="Error instead of skipping sessions if an interpreter can not be located.",
+    ),
+    *_option_set.make_flag_pair(
+        "error_on_external_run",
+        ("--error-on-external-run",),
+        ("--no-error-on-external-run",),
+        group="secondary",
+        help="Error if run() is used to execute a program that isn't installed in a session's virtualenv.",
+    ),
+    _option_set.Option(
+        "install_only",
+        "--install-only",
+        group="secondary",
+        action="store_true",
+        help="Skip session.run invocations in the Noxfile.",
+    ),
+    _option_set.Option(
+        "report",
+        "--report",
+        group="secondary",
+        noxfile=True,
+        help="Output a report of all sessions to the given filename.",
+    ),
+    _option_set.Option(
+        "nocolor",
+        "--nocolor",
+        "--no-color",
+        group="secondary",
+        default=lambda: "NO_COLOR" in os.environ,
+        action="store_true",
+        help="Disable all color output.",
+    ),
+    _option_set.Option(
+        "forcecolor",
+        "--forcecolor",
+        "--force-color",
+        group="secondary",
+        default=False,
+        action="store_true",
+        help="Force color output, even if stdout is not an interactive terminal.",
+    ),
+    _option_set.Option(
+        "color", "--color", hidden=True, finalizer_func=_color_finalizer
+    ),
+)
+
+
+"""Options that are configurable in the Noxfile.
+
+By setting properties on ``nox.options`` you can specify command line
+arguments in your Noxfile. If an argument is specified in both the Noxfile
+and on the command line, the command line arguments take precedence.
 
-    sessions = None
-    keywords = None
-    envdir = None
-    reuse_existing_virtualenvs = False
-    stop_on_first_error = False
-    error_on_missing_interpreters = False
-    error_on_external_run = False
-    install_only = False
-    report = None
+See :doc:`usage` for more details on these settings and their effect.
+"""
+noxfile_options = options.noxfile_namespace()
diff --git a/nox/tasks.py b/nox/tasks.py
index 5256b42..0d8b174 100644
--- a/nox/tasks.py
+++ b/nox/tasks.py
@@ -19,6 +19,7 @@ import os
 
 from colorlog.escape_codes import parse_colors
 
+import nox
 from nox import _options
 from nox import registry
 from nox.logger import logger
@@ -67,7 +68,7 @@ def merge_noxfile_options(module, global_config):
         module (module): The Noxfile module.
         global_config (~nox.main.GlobalConfig): The global configuration.
     """
-    global_config.merge_from_options(_options.options)
+    _options.options.merge_namespaces(global_config, nox.options)
     return module
 
 
diff --git a/noxfile.py b/noxfile.py
index 92a3451..b535358 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -75,9 +75,3 @@ def docs(session):
         sphinx_args.insert(0, "--open-browser")
 
     session.run(sphinx_cmd, *sphinx_args)
-
-
-@nox.session
-@nox.parametrize("django", [nox.param("1.9", id="old"), nox.param("2.0", id="new")])
-def django(session, django):
-    pass


Test patch:
diff --git a/tests/resources/noxfile_options.py b/tests/resources/noxfile_options.py
new file mode 100644
index 0000000..fb32d42
--- /dev/null
+++ b/tests/resources/noxfile_options.py
@@ -0,0 +1,23 @@
+# Copyright 2018 Alethea Katherine Flowers
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import nox
+
+nox.options.reuse_existing_virtualenvs = True
+nox.options.sessions = ["test"]
+
+
+@nox.session(py=False)
+def test(unused_session):
+    print("okay")
diff --git a/tests/test__option_set.py b/tests/test__option_set.py
new file mode 100644
index 0000000..323467b
--- /dev/null
+++ b/tests/test__option_set.py
@@ -0,0 +1,47 @@
+# Copyright 2019 Alethea Katherine Flowers
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+from nox import _option_set
+
+
+# The vast majority of _option_set is tested by test_main, but the test helper
+# :func:`OptionSet.namespace` needs a bit of help to get to full coverage.
+
+
+class TestOptionSet:
+    def test_namespace(self):
+        optionset = _option_set.OptionSet()
+        optionset.add_options(_option_set.Option("option_a", default="meep"))
+
+        namespace = optionset.namespace()
+
+        assert hasattr(namespace, "option_a")
+        assert not hasattr(namespace, "non_existant_option")
+        assert namespace.option_a == "meep"
+
+    def test_namespace_values(self):
+        optionset = _option_set.OptionSet()
+        optionset.add_options(_option_set.Option("option_a", default="meep"))
+
+        namespace = optionset.namespace(option_a="moop")
+
+        assert namespace.option_a == "moop"
+
+    def test_namespace_non_existant_options_with_values(self):
+        optionset = _option_set.OptionSet()
+
+        with pytest.raises(KeyError):
+            optionset.namespace(non_existant_option="meep")
diff --git a/tests/test_main.py b/tests/test_main.py
index 861d6a8..d091fcc 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -12,7 +12,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import argparse
 import os
 import sys
 from unittest import mock
@@ -32,113 +31,14 @@ RESOURCES = os.path.join(os.path.dirname(__file__), "resources")
 VERSION = pkg_resources.get_distribution("nox").version
 
 
-class TestGlobalConfig:
-    def make_args(self):
-        return argparse.Namespace(
-            noxfile="noxfile",
-            envdir="dir",
-            sessions=["1", "2"],
-            keywords="red and blue",
-            list_sessions=False,
-            reuse_existing_virtualenvs=False,
-            no_reuse_existing_virtualenvs=False,
-            stop_on_first_error=False,
-            no_stop_on_first_error=False,
-            error_on_missing_interpreters=False,
-            no_error_on_missing_interpreters=False,
-            error_on_external_run=False,
-            no_error_on_external_run=True,
-            install_only=False,
-            posargs=["a", "b", "c"],
-            report=None,
-            nocolor=False,
-            forcecolor=False,
-        )
-
-    def test_constructor(self):
-        args = self.make_args()
-        config = nox.__main__.GlobalConfig(args)
-
-        assert config.noxfile == "noxfile"
-        assert config.envdir == "dir"
-        assert config.sessions == ["1", "2"]
-        assert config.keywords == "red and blue"
-        assert config.list_sessions is False
-        assert config.reuse_existing_virtualenvs is False
-        assert config.no_reuse_existing_virtualenvs is False
-        assert config.stop_on_first_error is False
-        assert config.no_stop_on_first_error is False
-        assert config.error_on_missing_interpreters is False
-        assert config.no_error_on_missing_interpreters is False
-        assert config.install_only is False
-        assert config.posargs == ["a", "b", "c"]
-
-        args.posargs = ["--", "a", "b", "c"]
-        config = nox.__main__.GlobalConfig(args)
-        assert config.posargs == ["a", "b", "c"]
-
-    def test_merge_from_options_no_changes(self):
-        args = self.make_args()
-        config = nox.__main__.GlobalConfig(args)
-        original_values = vars(config).copy()
-        options = nox._options.options()
-
-        config.merge_from_options(options)
-
-        assert vars(config) == original_values
-
-    def test_merge_from_options_options_by_default(self):
-        args = self.make_args()
-        args.sessions = None
-        args.keywords = None
-        config = nox.__main__.GlobalConfig(args)
-        original_values = vars(config).copy()
-
-        options = nox._options.options()
-        options.sessions = ["1", "2"]
-        options.keywords = "one"
-        options.reuse_existing_virtualenvs = True
-        options.stop_on_first_error = True
-        options.error_on_missing_interpreters = True
-        options.install_only = True
-        options.report = "output.json"
-
-        config.merge_from_options(options)
-
-        assert vars(config) != original_values
-        assert config.sessions == ["1", "2"]
-        assert config.keywords == "one"
-        assert config.reuse_existing_virtualenvs is True
-        assert config.stop_on_first_error is True
-        assert config.error_on_missing_interpreters is True
-        assert config.report == "output.json"
-
-    def test_merge_from_options_args_precedence(self):
-        args = self.make_args()
-        args.sessions = ["1", "2"]
-        args.no_reuse_existing_virtualenvs = True
-        args.no_stop_on_first_error = True
-        args.no_error_on_missing_interpreters = True
-        args.report = "output.json"
-        config = nox.__main__.GlobalConfig(args)
-        original_values = vars(config).copy()
-
-        options = nox._options.options()
-        options.keywords = "one"
-        options.reuse_existing_virtualenvs = True
-        options.stop_on_first_error = True
-        options.error_on_missing_interpreters = True
-        options.install_only = True
-
-        config.merge_from_options(options)
-
-        assert vars(config) == original_values
+# This is needed because CI systems will mess up these tests due to the
+# way nox handles the --session parameter's default value. This avoids that
+# mess.
+os.environ.pop("NOXSESSION", None)
 
 
 def test_main_no_args(monkeypatch):
-    # Prevents any interference from outside
-    monkeypatch.delenv("NOXSESSION", raising=False)
-    sys.argv = [sys.executable]
+    monkeypatch.setattr(sys, "argv", [sys.executable])
     with mock.patch("nox.workflow.execute") as execute:
         execute.return_value = 0
 
@@ -152,8 +52,8 @@ def test_main_no_args(monkeypatch):
         config = execute.call_args[1]["global_config"]
         assert config.noxfile == "noxfile.py"
         assert config.sessions is None
-        assert config.reuse_existing_virtualenvs is False
-        assert config.stop_on_first_error is False
+        assert not config.reuse_existing_virtualenvs
+        assert not config.stop_on_first_error
         assert config.posargs == []
 
 
@@ -189,8 +89,10 @@ def test_main_long_form_args():
         assert config.posargs == []
 
 
-def test_main_short_form_args():
-    sys.argv = [sys.executable, "-f", "noxfile.py", "-s", "1", "2", "-r"]
+def test_main_short_form_args(monkeypatch):
+    monkeypatch.setattr(
+        sys, "argv", [sys.executable, "-f", "noxfile.py", "-s", "1", "2", "-r"]
+    )
     with mock.patch("nox.workflow.execute") as execute:
         execute.return_value = 0
 
@@ -207,8 +109,8 @@ def test_main_short_form_args():
         assert config.reuse_existing_virtualenvs is True
 
 
-def test_main_explicit_sessions():
-    sys.argv = [sys.executable, "-e", "1", "2"]
+def test_main_explicit_sessions(monkeypatch):
+    monkeypatch.setattr(sys, "argv", [sys.executable, "-e", "1", "2"])
     with mock.patch("nox.workflow.execute") as execute:
         execute.return_value = 0
 
@@ -223,8 +125,10 @@ def test_main_explicit_sessions():
         assert config.sessions == ["1", "2"]
 
 
-def test_main_explicit_sessions_with_spaces_in_names():
-    sys.argv = [sys.executable, "-e", "unit tests", "the unit tests"]
+def test_main_explicit_sessions_with_spaces_in_names(monkeypatch):
+    monkeypatch.setattr(
+        sys, "argv", [sys.executable, "-e", "unit tests", "the unit tests"]
+    )
     with mock.patch("nox.workflow.execute") as execute:
         execute.return_value = 0
 
@@ -244,7 +148,8 @@ def test_main_explicit_sessions_with_spaces_in_names():
 )
 def test_main_session_from_nox_env_var(monkeypatch, env, sessions):
     monkeypatch.setenv("NOXSESSION", env)
-    sys.argv = [sys.executable]
+    monkeypatch.setattr(sys, "argv", [sys.executable])
+
     with mock.patch("nox.workflow.execute") as execute:
         execute.return_value = 0
 
@@ -261,8 +166,8 @@ def test_main_session_from_nox_env_var(monkeypatch, env, sessions):
             assert session in config.sessions
 
 
-def test_main_positional_args():
-    sys.argv = [sys.executable, "1", "2", "3"]
+def test_main_positional_args(monkeypatch):
+    monkeypatch.setattr(sys, "argv", [sys.executable, "1", "2", "3"])
     with mock.patch("nox.workflow.execute") as execute:
         execute.return_value = 0
 
@@ -277,8 +182,8 @@ def test_main_positional_args():
         assert config.posargs == ["1", "2", "3"]
 
 
-def test_main_positional_with_double_hyphen():
-    sys.argv = [sys.executable, "--", "1", "2", "3"]
+def test_main_positional_with_double_hyphen(monkeypatch):
+    monkeypatch.setattr(sys, "argv", [sys.executable, "--", "1", "2", "3"])
     with mock.patch("nox.workflow.execute") as execute:
         execute.return_value = 0
 
@@ -293,8 +198,10 @@ def test_main_positional_with_double_hyphen():
         assert config.posargs == ["1", "2", "3"]
 
 
-def test_main_positional_flag_like_with_double_hyphen():
-    sys.argv = [sys.executable, "--", "1", "2", "3", "-f", "--baz"]
+def test_main_positional_flag_like_with_double_hyphen(monkeypatch):
+    monkeypatch.setattr(
+        sys, "argv", [sys.executable, "--", "1", "2", "3", "-f", "--baz"]
+    )
     with mock.patch("nox.workflow.execute") as execute:
         execute.return_value = 0
 
@@ -309,8 +216,8 @@ def test_main_positional_flag_like_with_double_hyphen():
         assert config.posargs == ["1", "2", "3", "-f", "--baz"]
 
 
-def test_main_version(capsys):
-    sys.argv = [sys.executable, "--version"]
+def test_main_version(capsys, monkeypatch):
+    monkeypatch.setattr(sys, "argv", [sys.executable, "--version"])
 
     with contexter.ExitStack() as stack:
         execute = stack.enter_context(mock.patch("nox.workflow.execute"))
@@ -322,8 +229,8 @@ def test_main_version(capsys):
         execute.assert_not_called()
 
 
-def test_main_help(capsys):
-    sys.argv = [sys.executable, "--help"]
+def test_main_help(capsys, monkeypatch):
+    monkeypatch.setattr(sys, "argv", [sys.executable, "--help"])
 
     with contexter.ExitStack() as stack:
         execute = stack.enter_context(mock.patch("nox.workflow.execute"))
@@ -335,8 +242,8 @@ def test_main_help(capsys):
         execute.assert_not_called()
 
 
-def test_main_failure():
-    sys.argv = [sys.executable]
+def test_main_failure(monkeypatch):
+    monkeypatch.setattr(sys, "argv", [sys.executable])
     with mock.patch("nox.workflow.execute") as execute:
         execute.return_value = 1
         with mock.patch.object(sys, "exit") as exit:
@@ -344,14 +251,18 @@ def test_main_failure():
             exit.assert_called_once_with(1)
 
 
-def test_main_nested_config(capsys):
-    sys.argv = [
-        "nox",
-        "--noxfile",
-        os.path.join(RESOURCES, "noxfile_nested.py"),
-        "-s",
-        "snack(cheese='cheddar')",
-    ]
+def test_main_nested_config(capsys, monkeypatch):
+    monkeypatch.setattr(
+        sys,
+        "argv",
+        [
+            "nox",
+            "--noxfile",
+            os.path.join(RESOURCES, "noxfile_nested.py"),
+            "-s",
+            "snack(cheese='cheddar')",
+        ],
+    )
 
     with mock.patch("sys.exit") as sys_exit:
         nox.__main__.main()
@@ -361,14 +272,18 @@ def test_main_nested_config(capsys):
         sys_exit.assert_called_once_with(0)
 
 
-def test_main_session_with_names(capsys):
-    sys.argv = [
-        "nox",
-        "--noxfile",
-        os.path.join(RESOURCES, "noxfile_spaces.py"),
-        "-s",
-        "cheese list(cheese='cheddar')",
-    ]
+def test_main_session_with_names(capsys, monkeypatch):
+    monkeypatch.setattr(
+        sys,
+        "argv",
+        [
+            "nox",
+            "--noxfile",
+            os.path.join(RESOURCES, "noxfile_spaces.py"),
+            "-s",
+            "cheese list(cheese='cheddar')",
+        ],
+    )
 
     with mock.patch("sys.exit") as sys_exit:
         nox.__main__.main()
@@ -376,3 +291,131 @@ def test_main_session_with_names(capsys):
         assert stdout == "Noms, cheddar so good!\n"
         assert "Session cheese list(cheese='cheddar') was successful." in stderr
         sys_exit.assert_called_once_with(0)
+
+
+def test_main_noxfile_options(monkeypatch):
+    monkeypatch.setattr(
+        sys,
+        "argv",
+        [
+            "nox",
+            "-l",
+            "-s",
+            "test",
+            "--noxfile",
+            os.path.join(RESOURCES, "noxfile_options.py"),
+        ],
+    )
+
+    with mock.patch("nox.tasks.honor_list_request") as honor_list_request:
+        honor_list_request.return_value = 0
+
+        with mock.patch("sys.exit"):
+            nox.__main__.main()
+
+        assert honor_list_request.called
+
+        # Verify that the config looks correct.
+        config = honor_list_request.call_args[1]["global_config"]
+        assert config.reuse_existing_virtualenvs is True
+
+
+def test_main_noxfile_options_disabled_by_flag(monkeypatch):
+    monkeypatch.setattr(
+        sys,
+        "argv",
+        [
+            "nox",
+            "-l",
+            "-s",
+            "test",
+            "--no-reuse-existing-virtualenvs",
+            "--noxfile",
+            os.path.join(RESOURCES, "noxfile_options.py"),
+        ],
+    )
+
+    with mock.patch("nox.tasks.honor_list_request") as honor_list_request:
+        honor_list_request.return_value = 0
+
+        with mock.patch("sys.exit"):
+            nox.__main__.main()
+
+        assert honor_list_request.called
+
+        # Verify that the config looks correct.
+        config = honor_list_request.call_args[1]["global_config"]
+        assert config.reuse_existing_virtualenvs is False
+
+
+def test_main_noxfile_options_sessions(monkeypatch):
+    monkeypatch.setattr(
+        sys,
+        "argv",
+        ["nox", "-l", "--noxfile", os.path.join(RESOURCES, "noxfile_options.py")],
+    )
+
+    with mock.patch("nox.tasks.honor_list_request") as honor_list_request:
+        honor_list_request.return_value = 0
+
+        with mock.patch("sys.exit"):
+            nox.__main__.main()
+
+        assert honor_list_request.called
+
+        # Verify that the config looks correct.
+        config = honor_list_request.call_args[1]["global_config"]
+        assert config.sessions == ["test"]
+
+
+@pytest.mark.parametrize(("isatty_value", "expected"), [(True, True), (False, False)])
+def test_main_color_from_isatty(monkeypatch, isatty_value, expected):
+    monkeypatch.setattr(sys, "argv", [sys.executable])
+    with mock.patch("nox.workflow.execute") as execute:
+        execute.return_value = 0
+        with mock.patch("sys.stderr.isatty") as isatty:
+            isatty.return_value = isatty_value
+
+            # Call the main function.
+            with mock.patch.object(sys, "exit"):
+                nox.__main__.main()
+
+            config = execute.call_args[1]["global_config"]
+            assert config.color == expected
+
+
+@pytest.mark.parametrize(
+    ("color_opt", "expected"),
+    [
+        ("--forcecolor", True),
+        ("--nocolor", False),
+        ("--force-color", True),
+        ("--no-color", False),
+    ],
+)
+def test_main_color_options(monkeypatch, color_opt, expected):
+    monkeypatch.setattr(sys, "argv", [sys.executable, color_opt])
+    with mock.patch("nox.workflow.execute") as execute:
+        execute.return_value = 0
+
+        # Call the main function.
+        with mock.patch.object(sys, "exit"):
+            nox.__main__.main()
+
+        config = execute.call_args[1]["global_config"]
+        assert config.color == expected
+
+
+def test_main_color_conflict(capsys, monkeypatch):
+    monkeypatch.setattr(sys, "argv", [sys.executable, "--forcecolor", "--nocolor"])
+    with mock.patch("nox.workflow.execute") as execute:
+        execute.return_value = 1
+
+        # Call the main function.
+        with mock.patch.object(sys, "exit") as exit:
+            nox.__main__.main()
+            exit.assert_called_with(1)
+
+    _, err = capsys.readouterr()
+
+    assert "color" in err
diff --git a/tests/test_sessions.py b/tests/test_sessions.py
index 21bc66a..7814448 100644
--- a/tests/test_sessions.py
+++ b/tests/test_sessions.py
@@ -20,6 +20,7 @@ from unittest import mock
 
 import pytest
 
+from nox import _options
 import nox.command
 from nox.logger import logger
 import nox.manifest
@@ -62,7 +63,7 @@ class TestSession:
             name="test",
             signatures=["test"],
             func=func,
-            global_config=argparse.Namespace(
+            global_config=_options.options.namespace(
                 posargs=mock.sentinel.posargs,
                 error_on_external_run=False,
                 install_only=False,
@@ -206,7 +207,7 @@ class TestSession:
             name="test",
             signatures=["test"],
             func=mock.sentinel.func,
-            global_config=argparse.Namespace(posargs=mock.sentinel.posargs),
+            global_config=_options.options.namespace(posargs=mock.sentinel.posargs),
             manifest=mock.create_autospec(nox.manifest.Manifest),
         )
         runner.venv = mock.create_autospec(nox.virtualenv.VirtualEnv)
@@ -228,7 +229,7 @@ class TestSession:
             name="test",
             signatures=["test"],
             func=mock.sentinel.func,
-            global_config=argparse.Namespace(posargs=mock.sentinel.posargs),
+            global_config=_options.options.namespace(posargs=mock.sentinel.posargs),
             manifest=mock.create_autospec(nox.manifest.Manifest),
         )
         runner.venv = mock.create_autospec(nox.virtualenv.VirtualEnv)
@@ -301,7 +302,7 @@ class TestSessionRunner:
             name="test",
             signatures=["test(1, 2)"],
             func=func,
-            global_config=argparse.Namespace(
+            global_config=_options.options.namespace(
                 noxfile=os.path.join(os.getcwd(), "noxfile.py"),
                 envdir="envdir",
                 posargs=mock.sentinel.posargs,
diff --git a/tests/test_tasks.py b/tests/test_tasks.py
index c4d32d4..0801d19 100644
--- a/tests/test_tasks.py
+++ b/tests/test_tasks.py
@@ -23,6 +23,7 @@ from unittest import mock
 import pytest
 
 import nox
+from nox import _options
 from nox import sessions
 from nox import tasks
 from nox.manifest import Manifest
@@ -39,7 +40,7 @@ session_func.python = None
 
 
 def test_load_nox_module():
-    config = argparse.Namespace(noxfile=os.path.join(RESOURCES, "noxfile.py"))
+    config = _options.options.namespace(noxfile=os.path.join(RESOURCES, "noxfile.py"))
     noxfile_module = tasks.load_nox_module(config)
     assert noxfile_module.SIGIL == "123"
 
@@ -50,16 +51,16 @@ def test_load_nox_module_expandvars():
     # %TEMP% to point to the noxfile.py
     with mock.patch.dict(os.environ, {"RESOURCES_PATH": RESOURCES}):
         if platform.system().lower().startswith("win"):
-            config = argparse.Namespace(noxfile="%RESOURCES_PATH%/noxfile.py")
+            config = _options.options.namespace(noxfile="%RESOURCES_PATH%/noxfile.py")
         else:
-            config = argparse.Namespace(noxfile="${RESOURCES_PATH}/noxfile.py")
+            config = _options.options.namespace(noxfile="${RESOURCES_PATH}/noxfile.py")
         noxfile_module = tasks.load_nox_module(config)
     assert noxfile_module.__file__ == os.path.join(RESOURCES, "noxfile.py")
     assert noxfile_module.SIGIL == "123"
 
 
 def test_load_nox_module_not_found():
-    config = argparse.Namespace(noxfile="bogus.py")
+    config = _options.options.namespace(noxfile="bogus.py")
     assert tasks.load_nox_module(config) == 2
 
 
@@ -84,7 +85,7 @@ def test_discover_session_functions_decorator():
     mock_module = argparse.Namespace(
         __name__=foo.__module__, foo=foo, bar=bar, notasession=notasession
     )
-    config = argparse.Namespace(sessions=(), keywords=())
+    config = _options.options.namespace(sessions=(), keywords=())
 
     # Get the manifest and establish that it looks like what we expect.
     manifest = tasks.discover_manifest(mock_module, config)
@@ -94,7 +95,7 @@ def test_discover_session_functions_decorator():
 
 
 def test_filter_manifest():
-    config = argparse.Namespace(sessions=(), keywords=())
+    config = _options.options.namespace(sessions=(), keywords=())
     manifest = Manifest({"foo": session_func, "bar": session_func}, config)
     return_value = tasks.filter_manifest(manifest, config)
     assert return_value is manifest
@@ -102,14 +103,14 @@ def test_filter_manifest():
 
 
 def test_filter_manifest_not_found():
-    config = argparse.Namespace(sessions=("baz",), keywords=())
+    config = _options.options.namespace(sessions=("baz",), keywords=())
     manifest = Manifest({"foo": session_func, "bar": session_func}, config)
     return_value = tasks.filter_manifest(manifest, config)
     assert return_value == 3
 
 
 def test_filter_manifest_keywords():
-    config = argparse.Namespace(sessions=(), keywords="foo or bar")
+    config = _options.options.namespace(sessions=(), keywords="foo or bar")
     manifest = Manifest(
         {"foo": session_func, "bar": session_func, "baz": session_func}, config
     )
@@ -119,7 +120,7 @@ def test_filter_manifest_keywords():
 
 
 def test_honor_list_request_noop():
-    config = argparse.Namespace(list_sessions=False)
+    config = _options.options.namespace(list_sessions=False)
     manifest = {"thing": mock.sentinel.THING}
     return_value = tasks.honor_list_request(manifest, global_config=config)
     assert return_value is manifest
@@ -127,7 +128,9 @@ def test_honor_list_request_noop():
 
 @pytest.mark.parametrize("description", [None, "bar"])
 def test_honor_list_request(description):
-    config = argparse.Namespace(list_sessions=True, noxfile="noxfile.py", color=False)
+    config = _options.options.namespace(
+        list_sessions=True, noxfile="noxfile.py", color=False
+    )
     manifest = mock.create_autospec(Manifest)
     manifest.list_all_sessions.return_value = [
         (argparse.Namespace(friendly_name="foo", description=description), True)
@@ -137,7 +140,9 @@ def test_honor_list_request(description):
 
 
 def test_honor_list_request_skip_and_selected(capsys):
-    config = argparse.Namespace(list_sessions=True, noxfile="noxfile.py", color=False)
+    config = _options.options.namespace(
+        list_sessions=True, noxfile="noxfile.py", color=False
+    )
     manifest = mock.create_autospec(Manifest)
     manifest.list_all_sessions.return_value = [
         (argparse.Namespace(friendly_name="foo", description=None), True),
@@ -153,14 +158,14 @@ def test_honor_list_request_skip_and_selected(capsys):
 
 
 def test_verify_manifest_empty():
-    config = argparse.Namespace(sessions=(), keywords=())
+    config = _options.options.namespace(sessions=(), keywords=())
     manifest = Manifest({}, config)
     return_value = tasks.verify_manifest_nonempty(manifest, global_config=config)
     assert return_value == 3
 
 
 def test_verify_manifest_nonempty():
-    config = argparse.Namespace(sessions=(), keywords=())
+    config = _options.options.namespace(sessions=(), keywords=())
     manifest = Manifest({"session": session_func}, config)
     return_value = tasks.verify_manifest_nonempty(manifest, global_config=config)
     assert return_value == manifest
@@ -168,7 +173,7 @@ def test_verify_manifest_nonempty():
 
 def test_run_manifest():
     # Set up a valid manifest.
-    config = argparse.Namespace(stop_on_first_error=False)
+    config = _options.options.namespace(stop_on_first_error=False)
     sessions_ = [
         mock.Mock(spec=sessions.SessionRunner),
         mock.Mock(spec=sessions.SessionRunner),
@@ -196,7 +201,7 @@ def test_run_manifest():
 
 def test_run_manifest_abort_on_first_failure():
     # Set up a valid manifest.
-    config = argparse.Namespace(stop_on_first_error=True)
+    config = _options.options.namespace(stop_on_first_error=True)
     sessions_ = [
         mock.Mock(spec=sessions.SessionRunner),
         mock.Mock(spec=sessions.SessionRunner),
@@ -252,7 +257,7 @@ def test_print_summary():
 
 
 def test_create_report_noop():
-    config = argparse.Namespace(report=None)
+    config = _options.options.namespace(report=None)
     with mock.patch.object(io, "open", autospec=True) as open_:
         results = tasks.create_report(mock.sentinel.RESULTS, config)
         assert not open_.called
@@ -260,7 +265,7 @@ def test_create_report_noop():
 
 
 def test_create_report():
-    config = argparse.Namespace(report="/path/to/report")
+    config = _options.options.namespace(report="/path/to/report")
     results = [
         sessions.Result(
             session=argparse.Namespace(