208 lines
6.5 KiB
Python
208 lines
6.5 KiB
Python
# -*- test-case-name: twisted.application.twist.test.test_options -*-
|
|
# Copyright (c) Twisted Matrix Laboratories.
|
|
# See LICENSE for details.
|
|
|
|
"""
|
|
Command line options for C{twist}.
|
|
"""
|
|
|
|
import typing
|
|
from sys import stderr, stdout
|
|
from textwrap import dedent
|
|
from typing import Callable, Iterable, Mapping, Optional, Sequence, Tuple, cast
|
|
|
|
from twisted.copyright import version
|
|
from twisted.internet.interfaces import IReactorCore
|
|
from twisted.logger import (
|
|
InvalidLogLevelError,
|
|
LogLevel,
|
|
jsonFileLogObserver,
|
|
textFileLogObserver,
|
|
)
|
|
from twisted.plugin import getPlugins
|
|
from twisted.python.usage import Options, UsageError
|
|
from ..reactors import NoSuchReactor, getReactorTypes, installReactor
|
|
from ..runner._exit import ExitStatus, exit
|
|
from ..service import IServiceMaker
|
|
|
|
openFile = open
|
|
|
|
|
|
def _update_doc(opt: Callable[["TwistOptions", str], None], **kwargs: str) -> None:
|
|
"""
|
|
Update the docstring of a method that implements an option.
|
|
The string is dedented and the given keyword arguments are substituted.
|
|
"""
|
|
opt.__doc__ = dedent(opt.__doc__ or "").format(**kwargs)
|
|
|
|
|
|
class TwistOptions(Options):
|
|
"""
|
|
Command line options for C{twist}.
|
|
"""
|
|
|
|
defaultReactorName = "default"
|
|
defaultLogLevel = LogLevel.info
|
|
|
|
def __init__(self) -> None:
|
|
Options.__init__(self)
|
|
|
|
self["reactorName"] = self.defaultReactorName
|
|
self["logLevel"] = self.defaultLogLevel
|
|
self["logFile"] = stdout
|
|
# An empty long description is explicitly set here as otherwise
|
|
# when executing from distributed trial twisted.python.usage will
|
|
# pull the description from `__main__` which is another entry point.
|
|
self.longdesc = ""
|
|
|
|
def getSynopsis(self) -> str:
|
|
return f"{Options.getSynopsis(self)} plugin [plugin_options]"
|
|
|
|
def opt_version(self) -> "typing.NoReturn":
|
|
"""
|
|
Print version and exit.
|
|
"""
|
|
exit(ExitStatus.EX_OK, f"{version}")
|
|
|
|
def opt_reactor(self, name: str) -> None:
|
|
"""
|
|
The name of the reactor to use.
|
|
(options: {options})
|
|
"""
|
|
# Actually actually actually install the reactor right at this very
|
|
# moment, before any other code (for example, a sub-command plugin)
|
|
# runs and accidentally imports and installs the default reactor.
|
|
try:
|
|
self["reactor"] = self.installReactor(name)
|
|
except NoSuchReactor:
|
|
raise UsageError(f"Unknown reactor: {name}")
|
|
else:
|
|
self["reactorName"] = name
|
|
|
|
_update_doc(
|
|
opt_reactor,
|
|
options=", ".join(f'"{rt.shortName}"' for rt in getReactorTypes()),
|
|
)
|
|
|
|
def installReactor(self, name: str) -> IReactorCore:
|
|
"""
|
|
Install the reactor.
|
|
"""
|
|
if name == self.defaultReactorName:
|
|
from twisted.internet import reactor
|
|
|
|
return cast(IReactorCore, reactor)
|
|
else:
|
|
return installReactor(name)
|
|
|
|
def opt_log_level(self, levelName: str) -> None:
|
|
"""
|
|
Set default log level.
|
|
(options: {options}; default: "{default}")
|
|
"""
|
|
try:
|
|
self["logLevel"] = LogLevel.levelWithName(levelName)
|
|
except InvalidLogLevelError:
|
|
raise UsageError(f"Invalid log level: {levelName}")
|
|
|
|
_update_doc(
|
|
opt_log_level,
|
|
options=", ".join(
|
|
f'"{constant.name}"' for constant in LogLevel.iterconstants()
|
|
),
|
|
default=defaultLogLevel.name,
|
|
)
|
|
|
|
def opt_log_file(self, fileName: str) -> None:
|
|
"""
|
|
Log to file. ("-" for stdout, "+" for stderr; default: "-")
|
|
"""
|
|
if fileName == "-":
|
|
self["logFile"] = stdout
|
|
return
|
|
|
|
if fileName == "+":
|
|
self["logFile"] = stderr
|
|
return
|
|
|
|
try:
|
|
self["logFile"] = openFile(fileName, "a")
|
|
except OSError as e:
|
|
exit(
|
|
ExitStatus.EX_IOERR,
|
|
f"Unable to open log file {fileName!r}: {e}",
|
|
)
|
|
|
|
def opt_log_format(self, format: str) -> None:
|
|
"""
|
|
Log file format.
|
|
(options: "text", "json"; default: "text" if the log file is a tty,
|
|
otherwise "json")
|
|
"""
|
|
format = format.lower()
|
|
|
|
if format == "text":
|
|
self["fileLogObserverFactory"] = textFileLogObserver
|
|
elif format == "json":
|
|
self["fileLogObserverFactory"] = jsonFileLogObserver
|
|
else:
|
|
raise UsageError(f"Invalid log format: {format}")
|
|
self["logFormat"] = format
|
|
|
|
_update_doc(opt_log_format)
|
|
|
|
def selectDefaultLogObserver(self) -> None:
|
|
"""
|
|
Set C{fileLogObserverFactory} to the default appropriate for the
|
|
chosen C{logFile}.
|
|
"""
|
|
if "fileLogObserverFactory" not in self:
|
|
logFile = self["logFile"]
|
|
|
|
if hasattr(logFile, "isatty") and logFile.isatty():
|
|
self["fileLogObserverFactory"] = textFileLogObserver
|
|
self["logFormat"] = "text"
|
|
else:
|
|
self["fileLogObserverFactory"] = jsonFileLogObserver
|
|
self["logFormat"] = "json"
|
|
|
|
def parseOptions(self, options: Optional[Sequence[str]] = None) -> None:
|
|
self.selectDefaultLogObserver()
|
|
|
|
Options.parseOptions(self, options=options)
|
|
|
|
if "reactor" not in self:
|
|
self["reactor"] = self.installReactor(self["reactorName"])
|
|
|
|
@property
|
|
def plugins(self) -> Mapping[str, IServiceMaker]:
|
|
if "plugins" not in self:
|
|
plugins = {}
|
|
for plugin in getPlugins(IServiceMaker):
|
|
plugins[plugin.tapname] = plugin
|
|
self["plugins"] = plugins
|
|
|
|
return cast(Mapping[str, IServiceMaker], self["plugins"])
|
|
|
|
@property
|
|
def subCommands(
|
|
self,
|
|
) -> Iterable[Tuple[str, None, Callable[[IServiceMaker], Options], str]]:
|
|
plugins = self.plugins
|
|
for name in sorted(plugins):
|
|
plugin = plugins[name]
|
|
|
|
# Don't pass plugin.options along in order to avoid resolving the
|
|
# options attribute right away, in case it's a property with a
|
|
# non-trivial getter (eg, one which imports modules).
|
|
def options(plugin: IServiceMaker = plugin) -> Options:
|
|
return cast(Options, plugin.options())
|
|
|
|
yield (plugin.tapname, None, options, plugin.description)
|
|
|
|
def postOptions(self) -> None:
|
|
Options.postOptions(self)
|
|
|
|
if self.subCommand is None:
|
|
raise UsageError("No plugin specified.")
|