RAHHH
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
# -*- 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.")
|
||||
Reference in New Issue
Block a user