123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207 |
- # -*- 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.")
|