_options.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. # -*- test-case-name: twisted.application.twist.test.test_options -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. Command line options for C{twist}.
  6. """
  7. from sys import stdout, stderr
  8. from textwrap import dedent
  9. from twisted.copyright import version
  10. from twisted.python.usage import Options, UsageError
  11. from twisted.logger import (
  12. LogLevel, InvalidLogLevelError,
  13. textFileLogObserver, jsonFileLogObserver,
  14. )
  15. from twisted.plugin import getPlugins
  16. from ..reactors import installReactor, NoSuchReactor, getReactorTypes
  17. from ..runner._exit import exit, ExitStatus
  18. from ..service import IServiceMaker
  19. openFile = open
  20. class TwistOptions(Options):
  21. """
  22. Command line options for C{twist}.
  23. """
  24. defaultReactorName = "default"
  25. defaultLogLevel = LogLevel.info
  26. def __init__(self):
  27. Options.__init__(self)
  28. self["reactorName"] = self.defaultReactorName
  29. self["logLevel"] = self.defaultLogLevel
  30. self["logFile"] = stdout
  31. def getSynopsis(self):
  32. return "{} plugin [plugin_options]".format(
  33. Options.getSynopsis(self)
  34. )
  35. def opt_version(self):
  36. """
  37. Print version and exit.
  38. """
  39. exit(ExitStatus.EX_OK, "{}".format(version))
  40. def opt_reactor(self, name):
  41. """
  42. The name of the reactor to use.
  43. (options: {options})
  44. """
  45. # Actually actually actually install the reactor right at this very
  46. # moment, before any other code (for example, a sub-command plugin)
  47. # runs and accidentally imports and installs the default reactor.
  48. try:
  49. self["reactor"] = self.installReactor(name)
  50. except NoSuchReactor:
  51. raise UsageError("Unknown reactor: {}".format(name))
  52. else:
  53. self["reactorName"] = name
  54. opt_reactor.__doc__ = dedent(opt_reactor.__doc__).format(
  55. options=", ".join(
  56. '"{}"'.format(rt.shortName) for rt in getReactorTypes()
  57. ),
  58. )
  59. def installReactor(self, name):
  60. """
  61. Install the reactor.
  62. """
  63. if name == self.defaultReactorName:
  64. from twisted.internet import reactor
  65. return reactor
  66. else:
  67. return installReactor(name)
  68. def opt_log_level(self, levelName):
  69. """
  70. Set default log level.
  71. (options: {options}; default: "{default}")
  72. """
  73. try:
  74. self["logLevel"] = LogLevel.levelWithName(levelName)
  75. except InvalidLogLevelError:
  76. raise UsageError("Invalid log level: {}".format(levelName))
  77. opt_log_level.__doc__ = dedent(opt_log_level.__doc__).format(
  78. options=", ".join(
  79. '"{}"'.format(l.name) for l in LogLevel.iterconstants()
  80. ),
  81. default=defaultLogLevel.name,
  82. )
  83. def opt_log_file(self, fileName):
  84. """
  85. Log to file. ("-" for stdout, "+" for stderr; default: "-")
  86. """
  87. if fileName == "-":
  88. self["logFile"] = stdout
  89. return
  90. if fileName == "+":
  91. self["logFile"] = stderr
  92. return
  93. try:
  94. self["logFile"] = openFile(fileName, "a")
  95. except EnvironmentError as e:
  96. exit(
  97. ExitStatus.EX_IOERR,
  98. "Unable to open log file {!r}: {}".format(fileName, e)
  99. )
  100. def opt_log_format(self, format):
  101. """
  102. Log file format.
  103. (options: "text", "json"; default: "text" if the log file is a tty,
  104. otherwise "json")
  105. """
  106. format = format.lower()
  107. if format == "text":
  108. self["fileLogObserverFactory"] = textFileLogObserver
  109. elif format == "json":
  110. self["fileLogObserverFactory"] = jsonFileLogObserver
  111. else:
  112. raise UsageError("Invalid log format: {}".format(format))
  113. self["logFormat"] = format
  114. opt_log_format.__doc__ = dedent(opt_log_format.__doc__)
  115. def selectDefaultLogObserver(self):
  116. """
  117. Set C{fileLogObserverFactory} to the default appropriate for the
  118. chosen C{logFile}.
  119. """
  120. if "fileLogObserverFactory" not in self:
  121. logFile = self["logFile"]
  122. if hasattr(logFile, "isatty") and logFile.isatty():
  123. self["fileLogObserverFactory"] = textFileLogObserver
  124. self["logFormat"] = "text"
  125. else:
  126. self["fileLogObserverFactory"] = jsonFileLogObserver
  127. self["logFormat"] = "json"
  128. def parseOptions(self, options=None):
  129. self.selectDefaultLogObserver()
  130. Options.parseOptions(self, options=options)
  131. if "reactor" not in self:
  132. self["reactor"] = self.installReactor(self["reactorName"])
  133. @property
  134. def plugins(self):
  135. if "plugins" not in self:
  136. plugins = {}
  137. for plugin in getPlugins(IServiceMaker):
  138. plugins[plugin.tapname] = plugin
  139. self["plugins"] = plugins
  140. return self["plugins"]
  141. @property
  142. def subCommands(self):
  143. plugins = self.plugins
  144. for name in sorted(plugins):
  145. plugin = plugins[name]
  146. yield (
  147. plugin.tapname,
  148. None,
  149. # Avoid resolving the options attribute right away, in case
  150. # it's a property with a non-trivial getter (eg, one which
  151. # imports modules).
  152. lambda plugin=plugin: plugin.options(),
  153. plugin.description,
  154. )
  155. def postOptions(self):
  156. Options.postOptions(self)
  157. if self.subCommand is None:
  158. raise UsageError("No plugin specified.")