2 changes: 0 additions & 2 deletions src/twisted/_threads/_convenience.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ def __init__(self):
"""
self.isSet = False


def set(self):
"""
Set the flag if it has not been set.
Expand All @@ -34,7 +33,6 @@ def set(self):
self.check()
self.isSet = True


def check(self):
"""
Check if the flag has been set.
Expand Down
1 change: 0 additions & 1 deletion src/twisted/_threads/_ithreads.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ class AlreadyQuit(Exception):
"""



class IWorker(Interface):
"""
A worker that can perform some work concurrently.
Expand Down
6 changes: 3 additions & 3 deletions src/twisted/_threads/_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

NoMoreWork = object()


@implementer(IWorker)
class MemoryWorker:
"""
Expand All @@ -30,7 +31,6 @@ def __init__(self, pending=list):
self._quit = Quit()
self._pending = pending()


def do(self, work):
"""
Queue some work for to perform later; see L{createMemoryWorker}.
Expand All @@ -40,7 +40,6 @@ def do(self, work):
self._quit.check()
self._pending.append(work)


def quit(self):
"""
Quit this worker.
Expand All @@ -49,7 +48,6 @@ def quit(self):
self._pending.append(NoMoreWork)



def createMemoryWorker():
"""
Create an L{IWorker} that does nothing but defer work, to be performed
Expand All @@ -59,12 +57,14 @@ def createMemoryWorker():
that will perform one element of that work.
@rtype: 2-L{tuple} of (L{IWorker}, L{callable})
"""

def perform():
if not worker._pending:
return False
if worker._pending[0] is NoMoreWork:
return False
worker._pending.pop(0)()
return True

worker = MemoryWorker()
return (worker, perform)
11 changes: 5 additions & 6 deletions src/twisted/_threads/_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,9 @@ def limitedWorkerCreator():
return None
return ThreadWorker(startThread, Queue())

team = Team(coordinator=LockWorker(Lock(), LocalStorage()),
createWorker=limitedWorkerCreator,
logException=err)
team = Team(
coordinator=LockWorker(Lock(), LocalStorage()),
createWorker=limitedWorkerCreator,
logException=err,
)
return team



18 changes: 4 additions & 14 deletions src/twisted/_threads/_team.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from ._convenience import Quit



class Statistics:
"""
Statistics about a L{Team}'s current activity.
Expand All @@ -32,14 +31,12 @@ class Statistics:
@type backloggedWorkCount: L{int}
"""

def __init__(self, idleWorkerCount, busyWorkerCount,
backloggedWorkCount):
def __init__(self, idleWorkerCount, busyWorkerCount, backloggedWorkCount):
self.idleWorkerCount = idleWorkerCount
self.busyWorkerCount = busyWorkerCount
self.backloggedWorkCount = backloggedWorkCount



@implementer(IWorker)
class Team:
"""
Expand Down Expand Up @@ -99,7 +96,6 @@ def __init__(self, coordinator, createWorker, logException):
self._shouldQuitCoordinator = False
self._toShrink = 0


def statistics(self):
"""
Gather information on the current status of this L{Team}.
Expand All @@ -108,7 +104,6 @@ def statistics(self):
"""
return Statistics(len(self._idle), self._busyCount, len(self._pending))


def grow(self, n):
"""
Increase the the number of idle workers by C{n}.
Expand All @@ -117,6 +112,7 @@ def grow(self, n):
@type n: L{int}
"""
self._quit.check()

@self._coordinator.do
def createOneWorker():
for x in range(n):
Expand All @@ -125,7 +121,6 @@ def createOneWorker():
return
self._recycleWorker(worker)


def shrink(self, n=None):
"""
Decrease the number of idle workers by C{n}.
Expand All @@ -137,7 +132,6 @@ def shrink(self, n=None):
self._quit.check()
self._coordinator.do(lambda: self._quitIdlers(n))


def _quitIdlers(self, n=None):
"""
The implmentation of C{shrink}, performed by the coordinator worker.
Expand All @@ -154,7 +148,6 @@ def _quitIdlers(self, n=None):
if self._shouldQuitCoordinator and self._busyCount == 0:
self._coordinator.quit()


def do(self, task):
"""
Perform some work in a worker created by C{createWorker}.
Expand All @@ -164,7 +157,6 @@ def do(self, task):
self._quit.check()
self._coordinator.do(lambda: self._coordinateThisTask(task))


def _coordinateThisTask(self, task):
"""
Select a worker to dispatch to, either an idle one or a new one, and
Expand All @@ -175,14 +167,14 @@ def _coordinateThisTask(self, task):
@param task: the task to dispatch
@type task: 0-argument callable
"""
worker = (self._idle.pop() if self._idle
else self._createWorker())
worker = self._idle.pop() if self._idle else self._createWorker()
if worker is None:
# The createWorker method may return None if we're out of resources
# to create workers.
self._pending.append(task)
return
self._busyCount += 1

@worker.do
def doWork():
try:
Expand All @@ -195,7 +187,6 @@ def idleAndPending():
self._busyCount -= 1
self._recycleWorker(worker)


def _recycleWorker(self, worker):
"""
Called only from coordinator.
Expand All @@ -217,7 +208,6 @@ def _recycleWorker(self, worker):
self._idle.remove(worker)
worker.quit()


def quit(self):
"""
Stop doing work and shut down all idle workers.
Expand Down
9 changes: 3 additions & 6 deletions src/twisted/_threads/_threadworker.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

_stop = object()


@implementer(IExclusiveWorker)
class ThreadWorker:
"""
Expand All @@ -40,11 +41,12 @@ def __init__(self, startThread, queue):
"""
self._q = queue
self._hasQuit = Quit()

def work():
for task in iter(queue.get, _stop):
task()
startThread(work)

startThread(work)

def do(self, task):
"""
Expand All @@ -55,7 +57,6 @@ def do(self, task):
self._hasQuit.check()
self._q.put(task)


def quit(self):
"""
Reject all future work and stop the thread started by C{__init__}.
Expand All @@ -66,7 +67,6 @@ def quit(self):
self._q.put(_stop)



@implementer(IExclusiveWorker)
class LockWorker:
"""
Expand All @@ -86,7 +86,6 @@ def __init__(self, lock, local):
self._lock = lock
self._local = local


def do(self, work):
"""
Do the given work on this thread, with the mutex acquired. If this is
Expand All @@ -112,11 +111,9 @@ def do(self, work):
else:
working.append(work)


def quit(self):
"""
Quit this L{LockWorker}.
"""
self._quit.set()
self._lock = None

4 changes: 0 additions & 4 deletions src/twisted/_threads/test/test_convenience.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ def test_isInitiallySet(self):
quit = Quit()
self.assertEqual(quit.isSet, False)


def test_setSetsSet(self):
"""
L{Quit.set} sets L{Quit.isSet} to L{True}.
Expand All @@ -33,15 +32,13 @@ def test_setSetsSet(self):
quit.set()
self.assertEqual(quit.isSet, True)


def test_checkDoesNothing(self):
"""
L{Quit.check} initially does nothing and returns L{None}.
"""
quit = Quit()
self.assertIs(quit.check(), None)


def test_checkAfterSetRaises(self):
"""
L{Quit.check} raises L{AlreadyQuit} if L{Quit.set} has been called.
Expand All @@ -50,7 +47,6 @@ def test_checkAfterSetRaises(self):
quit.set()
self.assertRaises(AlreadyQuit, quit.check)


def test_setTwiceRaises(self):
"""
L{Quit.set} raises L{AlreadyQuit} if it has been called previously.
Expand Down
4 changes: 2 additions & 2 deletions src/twisted/_threads/test/test_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ def test_createWorkerAndPerform(self):
self.assertEqual(performer(), True)
self.assertEqual(done, [3, 4])


def test_quitQuits(self):
"""
Calling C{quit} on the worker returned by L{createMemoryWorker} causes
Expand All @@ -43,8 +42,10 @@ def test_quitQuits(self):
"""
worker, performer = createMemoryWorker()
done = []

def moreWork():
done.append(7)

worker.do(moreWork)
worker.quit()
self.assertRaises(AlreadyQuit, worker.do, moreWork)
Expand All @@ -53,7 +54,6 @@ def moreWork():
self.assertEqual(done, [7])
self.assertEqual(performer(), False)


def test_performWhenNothingToDoYet(self):
"""
The C{perform} callable returned by L{createMemoryWorker} will return
Expand Down
30 changes: 11 additions & 19 deletions src/twisted/_threads/test/test_team.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from .. import IWorker, Team, createMemoryWorker, AlreadyQuit



class ContextualWorker(proxyForInterface(IWorker, "_realWorker")): # type: ignore[misc] # noqa
"""
A worker implementation that supplies a context.
Expand All @@ -28,7 +27,6 @@ def __init__(self, realWorker, **ctx):
self._realWorker = realWorker
self._context = ctx


def do(self, work):
"""
Perform the given work with the context given to __init__.
Expand All @@ -38,7 +36,6 @@ def do(self, work):
super(ContextualWorker, self).do(lambda: call(self._context, work))



class TeamTests(SynchronousTestCase):
"""
Tests for L{Team}
Expand Down Expand Up @@ -67,18 +64,21 @@ def createWorker():
self.allWorkersEver.append(cw)
self.allUnquitWorkers.append(cw)
realQuit = cw.quit

def quitAndRemove():
realQuit()
self.allUnquitWorkers.remove(cw)
self.activePerformers.remove(performer)

cw.quit = quitAndRemove
return cw

self.failures = []

def logException():
self.failures.append(Failure())
self.team = Team(coordinator, createWorker, logException)

self.team = Team(coordinator, createWorker, logException)

def coordinate(self):
"""
Expand All @@ -94,7 +94,6 @@ def coordinate(self):
did = True
return did


def performAllOutstandingWork(self):
"""
Perform all work on the coordinator and worker performers that needs to
Expand All @@ -108,22 +107,22 @@ def performAllOutstandingWork(self):
performer()
continuing = continuing or self.coordinate()


def test_doDoesWorkInWorker(self):
"""
L{Team.do} does the work in a worker created by the createWorker
callable.
"""

def something():
something.who = get("worker")

self.team.do(something)
self.coordinate()
self.assertEqual(self.team.statistics().busyWorkerCount, 1)
self.performAllOutstandingWork()
self.assertEqual(something.who, 1)
self.assertEqual(self.team.statistics().busyWorkerCount, 0)


def test_initialStatistics(self):
"""
L{Team.statistics} returns an object with idleWorkerCount,
Expand All @@ -134,7 +133,6 @@ def test_initialStatistics(self):
self.assertEqual(stats.busyWorkerCount, 0)
self.assertEqual(stats.backloggedWorkCount, 0)


def test_growCreatesIdleWorkers(self):
"""
L{Team.grow} increases the number of available idle workers.
Expand All @@ -143,7 +141,6 @@ def test_growCreatesIdleWorkers(self):
self.performAllOutstandingWork()
self.assertEqual(len(self.workerPerformers), 5)


def test_growCreateLimit(self):
"""
L{Team.grow} increases the number of available idle workers until the
Expand All @@ -155,7 +152,6 @@ def test_growCreateLimit(self):
self.assertEqual(len(self.allWorkersEver), 3)
self.assertEqual(self.team.statistics().idleWorkerCount, 3)


def test_shrinkQuitsWorkers(self):
"""
L{Team.shrink} will quit the given number of workers.
Expand All @@ -166,7 +162,6 @@ def test_shrinkQuitsWorkers(self):
self.performAllOutstandingWork()
self.assertEqual(len(self.allUnquitWorkers), 2)


def test_shrinkToZero(self):
"""
L{Team.shrink} with no arguments will stop all outstanding workers.
Expand All @@ -179,16 +174,17 @@ def test_shrinkToZero(self):
self.performAllOutstandingWork()
self.assertEqual(len(self.allUnquitWorkers), 0)


def test_moreWorkWhenNoWorkersAvailable(self):
"""
When no additional workers are available, the given work is backlogged,
and then performed later when the work was.
"""
self.team.grow(3)
self.coordinate()

def something():
something.times += 1

something.times = 0
self.assertEqual(self.team.statistics().idleWorkerCount, 3)
for i in range(3):
Expand All @@ -206,19 +202,17 @@ def something():
self.assertEqual(self.team.statistics().backloggedWorkCount, 0)
self.assertEqual(something.times, 4)


def test_exceptionInTask(self):
"""
When an exception is raised in a task passed to L{Team.do}, the
C{logException} given to the L{Team} at construction is invoked in the
exception context.
"""
self.team.do(lambda: 1/0)
self.team.do(lambda: 1 / 0)
self.performAllOutstandingWork()
self.assertEqual(len(self.failures), 1)
self.assertEqual(self.failures[0].type, ZeroDivisionError)


def test_quit(self):
"""
L{Team.quit} causes future invocations of L{Team.do} and L{Team.quit}
Expand All @@ -228,7 +222,6 @@ def test_quit(self):
self.assertRaises(AlreadyQuit, self.team.quit)
self.assertRaises(AlreadyQuit, self.team.do, list)


def test_quitQuits(self):
"""
L{Team.quit} causes all idle workers, as well as the coordinator
Expand All @@ -242,7 +235,6 @@ def test_quitQuits(self):
self.assertEqual(len(self.allUnquitWorkers), 0)
self.assertRaises(AlreadyQuit, self.coordinator.quit)


def test_quitQuitsLaterWhenBusy(self):
"""
L{Team.quit} causes all busy workers to be quit once they've finished
Expand All @@ -259,23 +251,23 @@ def test_quitQuitsLaterWhenBusy(self):
self.assertEqual(len(self.allUnquitWorkers), 0)
self.assertRaises(AlreadyQuit, self.coordinator.quit)


def test_quitConcurrentWithWorkHappening(self):
"""
If work happens after L{Team.quit} sets its C{Quit} flag, but before
any other work takes place, the L{Team} should still exit gracefully.
"""
self.team.do(list)
originalSet = self.team._quit.set

def performWorkConcurrently():
originalSet()
self.performAllOutstandingWork()

self.team._quit.set = performWorkConcurrently
self.team.quit()
self.assertRaises(AlreadyQuit, self.team.quit)
self.assertRaises(AlreadyQuit, self.team.do, list)


def test_shrinkWhenBusy(self):
"""
L{Team.shrink} will wait for busy workers to finish being busy and then
Expand Down
37 changes: 15 additions & 22 deletions src/twisted/_threads/test/test_threadworker.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,20 @@

from .. import ThreadWorker, LockWorker, AlreadyQuit


class FakeQueueEmpty(Exception):
"""
L{FakeQueue}'s C{get} has exhausted the queue.
"""



class WouldDeadlock(Exception):
"""
If this were a real lock, you'd be deadlocked because the lock would be
double-acquired.
"""



class FakeThread:
"""
A fake L{threading.Thread}.
Expand All @@ -47,15 +46,13 @@ def __init__(self, target):
self.target = target
self.started = False


def start(self):
"""
Set the "started" flag.
"""
self.started = True



class FakeQueue:
"""
A fake L{Queue} implementing C{put} and C{get}.
Expand All @@ -71,7 +68,6 @@ def __init__(self):
"""
self.items = []


def put(self, item):
"""
Put an item into the queue for later retrieval by L{FakeQueue.get}.
Expand All @@ -80,7 +76,6 @@ def put(self, item):
"""
self.items.append(item)


def get(self):
"""
Get an item.
Expand All @@ -92,7 +87,6 @@ def get(self):
return self.items.pop(0)



class FakeLock:
"""
A stand-in for L{threading.Lock}.
Expand All @@ -106,7 +100,6 @@ def __init__(self):
"""
self.acquired = False


def acquire(self):
"""
Acquire the lock. Raise an exception if the lock is already acquired.
Expand All @@ -115,7 +108,6 @@ def acquire(self):
raise WouldDeadlock()
self.acquired = True


def release(self):
"""
Release the lock. Raise an exception if the lock is not presently
Expand All @@ -126,7 +118,6 @@ def release(self):
self.acquired = False



class ThreadWorkerTests(SynchronousTestCase):
"""
Tests for L{ThreadWorker}.
Expand All @@ -138,13 +129,14 @@ def setUp(self):
"""
self.fakeThreads = []
self.fakeQueue = FakeQueue()

def startThread(target):
newThread = FakeThread(target=target)
newThread.start()
self.fakeThreads.append(newThread)
return newThread
self.worker = ThreadWorker(startThread, self.fakeQueue)

self.worker = ThreadWorker(startThread, self.fakeQueue)

def test_startsThreadAndPerformsWork(self):
"""
Expand All @@ -154,15 +146,16 @@ def test_startsThreadAndPerformsWork(self):
"""
self.assertEqual(len(self.fakeThreads), 1)
self.assertEqual(self.fakeThreads[0].started, True)

def doIt():
doIt.done = True

doIt.done = False
self.worker.do(doIt)
self.assertEqual(doIt.done, False)
self.assertRaises(FakeQueueEmpty, self.fakeThreads[0].target)
self.assertEqual(doIt.done, True)


def test_quitPreventsFutureCalls(self):
"""
L{ThreadWorker.quit} causes future calls to L{ThreadWorker.do} and
Expand All @@ -173,7 +166,6 @@ def test_quitPreventsFutureCalls(self):
self.assertRaises(AlreadyQuit, self.worker.do, list)



class LockWorkerTests(SynchronousTestCase):
"""
Tests for L{LockWorker}.
Expand All @@ -188,7 +180,6 @@ def test_fakeDeadlock(self):
lock.acquire()
self.assertRaises(WouldDeadlock, lock.acquire)


def test_fakeDoubleRelease(self):
"""
The L{FakeLock} test fixture will alert us if there's a potential
Expand All @@ -200,7 +191,6 @@ def test_fakeDoubleRelease(self):
self.assertEqual(None, lock.release())
self.assertRaises(ThreadError, lock.release)


def test_doExecutesImmediatelyWithLock(self):
"""
L{LockWorker.do} immediately performs the work it's given, while the
Expand All @@ -209,16 +199,17 @@ def test_doExecutesImmediatelyWithLock(self):
storage = local()
lock = FakeLock()
worker = LockWorker(lock, storage)

def work():
work.done = True
work.acquired = lock.acquired

work.done = False
worker.do(work)
self.assertEqual(work.done, True)
self.assertEqual(work.acquired, True)
self.assertEqual(lock.acquired, False)


def test_doUnwindsReentrancy(self):
"""
If L{LockWorker.do} is called recursively, it postpones the inner call
Expand All @@ -228,19 +219,20 @@ def test_doUnwindsReentrancy(self):
worker = LockWorker(lock, local())
levels = []
acquired = []

def work():
work.level += 1
levels.append(work.level)
acquired.append(lock.acquired)
if len(levels) < 2:
worker.do(work)
work.level -= 1

work.level = 0
worker.do(work)
self.assertEqual(levels, [1, 1])
self.assertEqual(acquired, [True, True])


def test_quit(self):
"""
L{LockWorker.quit} frees the resources associated with its lock and
Expand All @@ -257,7 +249,6 @@ def test_quit(self):
self.assertRaises(AlreadyQuit, worker.quit)
self.assertRaises(AlreadyQuit, worker.do, list)


def test_quitWhileWorking(self):
"""
If L{LockWorker.quit} is invoked during a call to L{LockWorker.do}, all
Expand All @@ -273,10 +264,13 @@ def phase1():
worker.quit()
self.assertRaises(AlreadyQuit, worker.do, list)
phase1.complete = True

phase1.complete = False

def phase2():
phase2.complete = True
phase2.acquired = lock.acquired

phase2.complete = False
worker.do(phase1)
self.assertEqual(phase1.complete, True)
Expand All @@ -286,23 +280,22 @@ def phase2():
gc.collect()
self.assertIs(ref(), None)


def test_quitWhileGettingLock(self):
"""
If L{LockWorker.do} is called concurrently with L{LockWorker.quit}, and
C{quit} wins the race before C{do} gets the lock attribute, then
L{AlreadyQuit} will be raised.
"""

class RacyLockWorker(LockWorker):
@property
def _lock(self):
self.quit()
return self.__dict__['_lock']
return self.__dict__["_lock"]

@_lock.setter
def _lock(self, value):
self.__dict__['_lock'] = value
self.__dict__["_lock"] = value

worker = RacyLockWorker(FakeLock(), local())
self.assertRaises(AlreadyQuit, worker.do, list)

2 changes: 1 addition & 1 deletion src/twisted/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@

from incremental import Version

__version__ = Version('Twisted', 20, 3, 0, dev=0)
__version__ = Version("Twisted", 20, 3, 0, dev=0)
__all__ = ["__version__"]
293 changes: 149 additions & 144 deletions src/twisted/application/app.py

Large diffs are not rendered by default.

411 changes: 207 additions & 204 deletions src/twisted/application/internet.py

Large diffs are not rendered by default.

20 changes: 11 additions & 9 deletions src/twisted/application/reactors.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,18 @@ class IReactorInstaller(Interface):
"""
Definition of a reactor which can probably be installed.
"""
shortName = Attribute("""

shortName = Attribute(
"""
A brief string giving the user-facing name of this reactor.
""")
"""
)

description = Attribute("""
description = Attribute(
"""
A longer string giving a user-facing description of this reactor.
""")
"""
)

def install():
"""
Expand All @@ -35,39 +40,35 @@ def install():
# can actually be used in the execution environment.



class NoSuchReactor(KeyError):
"""
Raised when an attempt is made to install a reactor which cannot be found.
"""



@implementer(IPlugin, IReactorInstaller)
class Reactor:
"""
@ivar moduleName: The fully-qualified Python name of the module of which
the install callable is an attribute.
"""

def __init__(self, shortName, moduleName, description):
self.shortName = shortName
self.moduleName = moduleName
self.description = description


def install(self):
namedAny(self.moduleName).install()



def getReactorTypes():
"""
Return an iterator of L{IReactorInstaller} plugins.
"""
return getPlugins(IReactorInstaller)



def installReactor(shortName):
"""
Install the reactor with the given C{shortName} attribute.
Expand All @@ -80,5 +81,6 @@ def installReactor(shortName):
if installer.shortName == shortName:
installer.install()
from twisted.internet import reactor

return reactor
raise NoSuchReactor(shortName)
65 changes: 32 additions & 33 deletions src/twisted/application/runner/_exit.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from constantly import Values, ValueConstant



def exit(status, message=None):
"""
Exit the python interpreter with the given status and an optional message.
Expand All @@ -38,33 +37,33 @@ def exit(status, message=None):
sysexit(code)



try:
import posix as Status
except ImportError:

class Status: # type: ignore[no-redef]
"""
Object to hang C{EX_*} values off of as a substitute for L{posix}.
"""

EX__BASE = 64

EX_OK = 0
EX_USAGE = EX__BASE
EX_DATAERR = EX__BASE + 1
EX_NOINPUT = EX__BASE + 2
EX_NOUSER = EX__BASE + 3
EX_NOHOST = EX__BASE + 4
EX_OK = 0
EX_USAGE = EX__BASE
EX_DATAERR = EX__BASE + 1
EX_NOINPUT = EX__BASE + 2
EX_NOUSER = EX__BASE + 3
EX_NOHOST = EX__BASE + 4
EX_UNAVAILABLE = EX__BASE + 5
EX_SOFTWARE = EX__BASE + 6
EX_OSERR = EX__BASE + 7
EX_OSFILE = EX__BASE + 8
EX_CANTCREAT = EX__BASE + 9
EX_IOERR = EX__BASE + 10
EX_TEMPFAIL = EX__BASE + 11
EX_PROTOCOL = EX__BASE + 12
EX_NOPERM = EX__BASE + 13
EX_CONFIG = EX__BASE + 14

EX_SOFTWARE = EX__BASE + 6
EX_OSERR = EX__BASE + 7
EX_OSFILE = EX__BASE + 8
EX_CANTCREAT = EX__BASE + 9
EX_IOERR = EX__BASE + 10
EX_TEMPFAIL = EX__BASE + 11
EX_PROTOCOL = EX__BASE + 12
EX_NOPERM = EX__BASE + 13
EX_CONFIG = EX__BASE + 14


class ExitStatus(Values):
Expand Down Expand Up @@ -120,19 +119,19 @@ class ExitStatus(Values):
@type EX_CONFIG: L{ValueConstant}
"""

EX_OK = ValueConstant(Status.EX_OK)
EX_USAGE = ValueConstant(Status.EX_USAGE)
EX_DATAERR = ValueConstant(Status.EX_DATAERR)
EX_NOINPUT = ValueConstant(Status.EX_NOINPUT)
EX_NOUSER = ValueConstant(Status.EX_NOUSER)
EX_NOHOST = ValueConstant(Status.EX_NOHOST)
EX_OK = ValueConstant(Status.EX_OK)
EX_USAGE = ValueConstant(Status.EX_USAGE)
EX_DATAERR = ValueConstant(Status.EX_DATAERR)
EX_NOINPUT = ValueConstant(Status.EX_NOINPUT)
EX_NOUSER = ValueConstant(Status.EX_NOUSER)
EX_NOHOST = ValueConstant(Status.EX_NOHOST)
EX_UNAVAILABLE = ValueConstant(Status.EX_UNAVAILABLE)
EX_SOFTWARE = ValueConstant(Status.EX_SOFTWARE)
EX_OSERR = ValueConstant(Status.EX_OSERR)
EX_OSFILE = ValueConstant(Status.EX_OSFILE)
EX_CANTCREAT = ValueConstant(Status.EX_CANTCREAT)
EX_IOERR = ValueConstant(Status.EX_IOERR)
EX_TEMPFAIL = ValueConstant(Status.EX_TEMPFAIL)
EX_PROTOCOL = ValueConstant(Status.EX_PROTOCOL)
EX_NOPERM = ValueConstant(Status.EX_NOPERM)
EX_CONFIG = ValueConstant(Status.EX_CONFIG)
EX_SOFTWARE = ValueConstant(Status.EX_SOFTWARE)
EX_OSERR = ValueConstant(Status.EX_OSERR)
EX_OSFILE = ValueConstant(Status.EX_OSFILE)
EX_CANTCREAT = ValueConstant(Status.EX_CANTCREAT)
EX_IOERR = ValueConstant(Status.EX_IOERR)
EX_TEMPFAIL = ValueConstant(Status.EX_TEMPFAIL)
EX_PROTOCOL = ValueConstant(Status.EX_PROTOCOL)
EX_NOPERM = ValueConstant(Status.EX_NOPERM)
EX_CONFIG = ValueConstant(Status.EX_CONFIG)
36 changes: 2 additions & 34 deletions src/twisted/application/runner/_pidfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from twisted.logger import Logger



class IPIDFile(Interface):
"""
Manages a file that remembers a process ID.
Expand All @@ -32,23 +31,20 @@ def read():
@raise ValueError: If this PID file's content is invalid.
"""


def writeRunningPID():
"""
Store the PID of the current process in this PID file.
@raise EnvironmentError: If this PID file cannot be written.
"""


def remove():
"""
Remove this PID file.
@raise EnvironmentError: If this PID file cannot be removed.
"""


def isRunning():
"""
Determine whether there is a running process corresponding to the PID
Expand All @@ -64,7 +60,6 @@ def isRunning():
for which there is no corresponding running process.
"""


def __enter__():
"""
Enter a context using this PIDFile.
Expand All @@ -75,7 +70,6 @@ def __enter__():
PID file is already running.
"""


def __exit__(excType, excValue, traceback):
"""
Exit a context using this PIDFile.
Expand All @@ -84,7 +78,6 @@ def __exit__(excType, excValue, traceback):
"""



@implementer(IPIDFile)
class PIDFile:
"""
Expand All @@ -97,7 +90,6 @@ class PIDFile:

_log = Logger()


@staticmethod
def _format(pid):
"""
Expand All @@ -109,8 +101,7 @@ def _format(pid):
@return: Formatted PID file contents.
@rtype: L{bytes}
"""
return u"{}\n".format(int(pid)).encode("utf-8")

return "{}\n".format(int(pid)).encode("utf-8")

def __init__(self, filePath):
"""
Expand All @@ -119,7 +110,6 @@ def __init__(self, filePath):
"""
self.filePath = filePath


def read(self):
pidString = b""
try:
Expand All @@ -138,7 +128,6 @@ def read(self):
"non-integer PID value in PID file: {!r}".format(pidString)
)


def _write(self, pid):
"""
Store a PID in this PID file.
Expand All @@ -150,15 +139,12 @@ def _write(self, pid):
"""
self.filePath.setContent(self._format(pid=pid))


def writeRunningPID(self):
self._write(getpid())


def remove(self):
self.filePath.remove()


def isRunning(self):
try:
pid = self.read()
Expand All @@ -172,7 +158,6 @@ def isRunning(self):
"isRunning is not implemented on {}".format(SYSTEM_NAME)
)


@staticmethod
def _pidIsRunningPOSIX(pid):
"""
Expand All @@ -193,17 +178,14 @@ def _pidIsRunningPOSIX(pid):
kill(pid, 0)
except OSError as e:
if e.errno == errno.ESRCH: # No such process
raise StalePIDFileError(
"PID file refers to non-existing process"
)
raise StalePIDFileError("PID file refers to non-existing process")
elif e.errno == errno.EPERM: # Not permitted to kill
return True
else:
raise
else:
return True


def __enter__(self):
try:
if self.isRunning():
Expand All @@ -213,12 +195,10 @@ def __enter__(self):
self.writeRunningPID()
return self


def __exit__(self, excType, excValue, traceback):
self.remove()



@implementer(IPIDFile)
class NonePIDFile:
"""
Expand All @@ -231,11 +211,9 @@ class NonePIDFile:
def __init__(self):
pass


def read(self):
raise NoPIDFound("PID file does not exist")


def _write(self, pid):
"""
Store a PID in this PID file.
Expand All @@ -249,54 +227,44 @@ def _write(self, pid):
"""
raise OSError(errno.EPERM, "Operation not permitted")


def writeRunningPID(self):
self._write(0)


def remove(self):
raise OSError(errno.ENOENT, "No such file or directory")


def isRunning(self):
return False


def __enter__(self):
return self


def __exit__(self, excType, excValue, traceback):
pass



nonePIDFile = NonePIDFile()



class AlreadyRunningError(Exception):
"""
Process is already running.
"""



class InvalidPIDFileError(Exception):
"""
PID file contents are invalid.
"""



class StalePIDFileError(Exception):
"""
PID file contents are valid, but there is no process with the referenced
PID.
"""



class NoPIDFound(Exception):
"""
No PID found in PID file.
Expand Down
36 changes: 15 additions & 21 deletions src/twisted/application/runner/_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,18 @@
from attr import attrib, attrs, Factory

from twisted.logger import (
globalLogBeginner, textFileLogObserver,
FilteringLogObserver, LogLevelFilterPredicate,
LogLevel, Logger,
globalLogBeginner,
textFileLogObserver,
FilteringLogObserver,
LogLevelFilterPredicate,
LogLevel,
Logger,
)

from ._exit import exit, ExitStatus
from ._pidfile import nonePIDFile, AlreadyRunningError, InvalidPIDFileError



@attrs(frozen=True)
class Runner:
"""
Expand Down Expand Up @@ -74,18 +76,17 @@ class Runner:

_log = Logger()

_reactor = attrib()
_pidFile = attrib(default=nonePIDFile)
_kill = attrib(default=False)
_defaultLogLevel = attrib(default=LogLevel.info)
_logFile = attrib(default=stderr)
_reactor = attrib()
_pidFile = attrib(default=nonePIDFile)
_kill = attrib(default=False)
_defaultLogLevel = attrib(default=LogLevel.info)
_logFile = attrib(default=stderr)
_fileLogObserverFactory = attrib(default=textFileLogObserver)
_whenRunning = attrib(default=lambda **_: None)
_whenRunningArguments = attrib(default=Factory(dict))
_reactorExited = attrib(default=lambda **_: None)
_whenRunning = attrib(default=lambda **_: None)
_whenRunningArguments = attrib(default=Factory(dict))
_reactorExited = attrib(default=lambda **_: None)
_reactorExitedArguments = attrib(default=Factory(dict))


def run(self):
"""
Run this command.
Expand All @@ -104,7 +105,6 @@ def run(self):
exit(ExitStatus.EX_CONFIG, "Already running.")
return # When testing, patched exit doesn't exit


def killIfRequested(self):
"""
If C{self._kill} is true, attempt to kill a running instance of the
Expand Down Expand Up @@ -134,7 +134,6 @@ def killIfRequested(self):
exit(ExitStatus.EX_OK)
return # When testing, patched exit doesn't exit


def startLogging(self):
"""
Start the L{twisted.logger} logging system.
Expand All @@ -149,13 +148,10 @@ def startLogging(self):
defaultLogLevel=self._defaultLogLevel
)

filteringObserver = FilteringLogObserver(
fileLogObserver, [logLevelPredicate]
)
filteringObserver = FilteringLogObserver(fileLogObserver, [logLevelPredicate])

globalLogBeginner.beginLoggingTo([filteringObserver])


def startReactor(self):
"""
Register C{self._whenRunning} with the reactor so that it is called
Expand All @@ -166,7 +162,6 @@ def startReactor(self):
self._log.info("Starting reactor...")
self._reactor.run()


def whenRunning(self):
"""
Call C{self._whenRunning} with C{self._whenRunningArguments}.
Expand All @@ -175,7 +170,6 @@ def whenRunning(self):
"""
self._whenRunning(**self._whenRunningArguments)


def reactorExited(self):
"""
Call C{self._reactorExited} with C{self._reactorExitedArguments}.
Expand Down
12 changes: 2 additions & 10 deletions src/twisted/application/runner/test/test_exit.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import twisted.trial.unittest



class ExitTests(twisted.trial.unittest.TestCase):
"""
Tests for L{exit}.
Expand All @@ -23,7 +22,6 @@ def setUp(self):
self.exit = DummyExit()
self.patch(_exit, "sysexit", self.exit)


def test_exitStatusInt(self):
"""
L{exit} given an L{int} status code will pass it to L{sys.exit}.
Expand All @@ -32,15 +30,13 @@ def test_exitStatusInt(self):
exit(status)
self.assertEqual(self.exit.arg, status)


def test_exitStatusStringNotInt(self):
"""
L{exit} given a L{str} status code that isn't a string integer raises
L{ValueError}.
"""
self.assertRaises(ValueError, exit, "foo")


def test_exitStatusStringInt(self):
"""
L{exit} given a L{str} status code that is a string integer passes the
Expand All @@ -49,7 +45,6 @@ def test_exitStatusStringInt(self):
exit("1234")
self.assertEqual(self.exit.arg, 1234)


def test_exitConstant(self):
"""
L{exit} given a L{ValueConstant} status code passes the corresponding
Expand All @@ -59,7 +54,6 @@ def test_exitConstant(self):
exit(status)
self.assertEqual(self.exit.arg, status.value)


def test_exitMessageZero(self):
"""
L{exit} given a status code of zero (C{0}) writes the given message to
Expand All @@ -73,7 +67,6 @@ def test_exitMessageZero(self):

self.assertEqual(out.getvalue(), message + "\n")


def test_exitMessageNonZero(self):
"""
L{exit} given a non-zero status code writes the given message to
Expand All @@ -88,18 +81,17 @@ def test_exitMessageNonZero(self):
self.assertEqual(out.getvalue(), message + "\n")



class DummyExit:
"""
Stub for L{sys.exit} that remembers whether it's been called and, if it
has, what argument it was given.
"""

def __init__(self):
self.exited = False


def __call__(self, arg=None):
assert not self.exited

self.arg = arg
self.arg = arg
self.exited = True
80 changes: 18 additions & 62 deletions src/twisted/application/runner/test/test_pidfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@

from ...runner import _pidfile
from .._pidfile import (
IPIDFile, PIDFile, NonePIDFile,
AlreadyRunningError, InvalidPIDFileError, StalePIDFileError,
IPIDFile,
PIDFile,
NonePIDFile,
AlreadyRunningError,
InvalidPIDFileError,
StalePIDFileError,
NoPIDFound,
)

Expand All @@ -45,6 +49,7 @@ def ifPlatformSupported(f):
@return: The wrapped callable.
"""

@wraps(f)
def wrapper(self, *args, **kwargs):
supported = platform.getType() == "posix"
Expand All @@ -54,17 +59,17 @@ def wrapper(self, *args, **kwargs):
else:
e = self.assertRaises(
(NotImplementedError, SkipTest, self.failureException),
f, self, *args, **kwargs
f,
self,
*args,
**kwargs,
)
if isinstance(e, NotImplementedError):
self.assertTrue(
str(e).startswith("isRunning is not implemented on ")
)
self.assertTrue(str(e).startswith("isRunning is not implemented on "))

return wrapper



class PIDFileTests(twisted.trial.unittest.TestCase):
"""
Tests for L{PIDFile}.
Expand All @@ -77,14 +82,12 @@ def test_interface(self):
pidFile = PIDFile(DummyFilePath())
verifyObject(IPIDFile, pidFile)


def test_formatWithPID(self):
"""
L{PIDFile._format} returns the expected format when given a PID.
"""
self.assertEqual(PIDFile._format(pid=1337), b"1337\n")


def test_readWithPID(self):
"""
L{PIDFile.read} returns the PID from the given file path.
Expand All @@ -95,7 +98,6 @@ def test_readWithPID(self):

self.assertEqual(pid, pidFile.read())


def test_readEmptyPID(self):
"""
L{PIDFile.read} raises L{InvalidPIDFileError} when given an empty file
Expand All @@ -106,11 +108,9 @@ def test_readEmptyPID(self):

e = self.assertRaises(InvalidPIDFileError, pidFile.read)
self.assertEqual(
str(e),
"non-integer PID value in PID file: {!r}".format(pidValue)
str(e), "non-integer PID value in PID file: {!r}".format(pidValue)
)


def test_readWithBogusPID(self):
"""
L{PIDFile.read} raises L{InvalidPIDFileError} when given an empty file
Expand All @@ -121,11 +121,9 @@ def test_readWithBogusPID(self):

e = self.assertRaises(InvalidPIDFileError, pidFile.read)
self.assertEqual(
str(e),
"non-integer PID value in PID file: {!r}".format(pidValue)
str(e), "non-integer PID value in PID file: {!r}".format(pidValue)
)


def test_readDoesntExist(self):
"""
L{PIDFile.read} raises L{NoPIDFound} when given a non-existing file
Expand All @@ -136,12 +134,12 @@ def test_readDoesntExist(self):
e = self.assertRaises(NoPIDFound, pidFile.read)
self.assertEqual(str(e), "PID file does not exist")


def test_readOpenRaisesOSErrorNotENOENT(self):
"""
L{PIDFile.read} re-raises L{OSError} if the associated C{errno} is
anything other than L{errno.ENOENT}.
"""

def oops(mode="r"):
raise OSError(errno.EIO, "I/O error")

Expand All @@ -152,7 +150,6 @@ def oops(mode="r"):
error = self.assertRaises(OSError, pidFile.read)
self.assertEqual(error.errno, errno.EIO)


def test_writePID(self):
"""
L{PIDFile._write} stores the given PID.
Expand All @@ -164,15 +161,13 @@ def test_writePID(self):

self.assertEqual(pidFile.read(), pid)


def test_writePIDInvalid(self):
"""
L{PIDFile._write} raises L{ValueError} when given an invalid PID.
"""
pidFile = PIDFile(DummyFilePath())

self.assertRaises(ValueError, pidFile._write, u"burp")

self.assertRaises(ValueError, pidFile._write, "burp")

def test_writeRunningPID(self):
"""
Expand All @@ -183,7 +178,6 @@ def test_writeRunningPID(self):

self.assertEqual(pidFile.read(), getpid())


def test_remove(self):
"""
L{PIDFile.remove} removes the PID file.
Expand All @@ -194,7 +188,6 @@ def test_remove(self):
pidFile.remove()
self.assertFalse(pidFile.filePath.exists())


@ifPlatformSupported
def test_isRunningDoesExist(self):
"""
Expand All @@ -210,7 +203,6 @@ def kill(pid, signal):

self.assertTrue(pidFile.isRunning())


@ifPlatformSupported
def test_isRunningThis(self):
"""
Expand All @@ -225,7 +217,6 @@ def test_isRunningThis(self):

self.assertTrue(pidFile.isRunning())


@ifPlatformSupported
def test_isRunningDoesNotExist(self):
"""
Expand All @@ -242,7 +233,6 @@ def kill(pid, signal):

self.assertRaises(StalePIDFileError, pidFile.isRunning)


@ifPlatformSupported
def test_isRunningNotAllowed(self):
"""
Expand All @@ -259,7 +249,6 @@ def kill(pid, signal):

self.assertTrue(pidFile.isRunning())


@ifPlatformSupported
def test_isRunningInit(self):
"""
Expand All @@ -283,7 +272,6 @@ def test_isRunningInit(self):

self.assertTrue(pidFile.isRunning())


@ifPlatformSupported
def test_isRunningUnknownErrno(self):
"""
Expand All @@ -300,7 +288,6 @@ def kill(pid, signal):

self.assertRaises(OSError, pidFile.isRunning)


def test_isRunningNoPIDFile(self):
"""
L{PIDFile.isRunning} returns false if the PID file doesn't exist.
Expand All @@ -309,7 +296,6 @@ def test_isRunningNoPIDFile(self):

self.assertFalse(pidFile.isRunning())


def test_contextManager(self):
"""
When used as a context manager, a L{PIDFile} will store the current pid
Expand All @@ -324,7 +310,6 @@ def test_contextManager(self):

self.assertFalse(pidFile.filePath.exists())


@ifPlatformSupported
def test_contextManagerDoesntExist(self):
"""
Expand All @@ -346,7 +331,6 @@ def kill(pid, signal):
with pidFile:
self.assertEqual(pidFile.read(), getpid())


@ifPlatformSupported
def test_contextManagerAlreadyRunning(self):
"""
Expand All @@ -367,7 +351,6 @@ def kill(pid, signal):
self.assertRaises(AlreadyRunningError, pidFile.__enter__)



class NonePIDFileTests(twisted.trial.unittest.TestCase):
"""
Tests for L{NonePIDFile}.
Expand All @@ -380,7 +363,6 @@ def test_interface(self):
pidFile = NonePIDFile()
verifyObject(IPIDFile, pidFile)


def test_read(self):
"""
L{NonePIDFile.read} raises L{NoPIDFound}.
Expand All @@ -390,7 +372,6 @@ def test_read(self):
e = self.assertRaises(NoPIDFound, pidFile.read)
self.assertEqual(str(e), "PID file does not exist")


def test_write(self):
"""
L{NonePIDFile._write} raises L{OSError} with an errno of L{errno.EPERM}.
Expand All @@ -400,7 +381,6 @@ def test_write(self):
error = self.assertRaises(OSError, pidFile._write, 0)
self.assertEqual(error.errno, errno.EPERM)


def test_writeRunningPID(self):
"""
L{NonePIDFile.writeRunningPID} raises L{OSError} with an errno of
Expand All @@ -411,7 +391,6 @@ def test_writeRunningPID(self):
error = self.assertRaises(OSError, pidFile.writeRunningPID)
self.assertEqual(error.errno, errno.EPERM)


def test_remove(self):
"""
L{NonePIDFile.remove} raises L{OSError} with an errno of L{errno.EPERM}.
Expand All @@ -421,7 +400,6 @@ def test_remove(self):
error = self.assertRaises(OSError, pidFile.remove)
self.assertEqual(error.errno, errno.ENOENT)


def test_isRunning(self):
"""
L{NonePIDFile.isRunning} returns L{False}.
Expand All @@ -430,7 +408,6 @@ def test_isRunning(self):

self.assertEqual(pidFile.isRunning(), False)


def test_contextManager(self):
"""
When used as a context manager, a L{NonePIDFile} doesn't raise, despite
Expand All @@ -442,7 +419,6 @@ def test_contextManager(self):
pass



@implementer(IFilePath)
class DummyFilePath:
"""
Expand All @@ -452,92 +428,72 @@ class DummyFilePath:
def __init__(self, content=None):
self.setContent(content)


def open(self, mode="r"):
if not self._exists:
raise OSError(errno.ENOENT, "No such file or directory")
return BytesIO(self.getContent())


def setContent(self, content):
self._exists = content is not None
self._content = content


def getContent(self):
return self._content


def remove(self):
self.setContent(None)


def exists(self):
return self._exists


def sep(self):
# IFilePath.sep
raise NotImplementedError("Unimplemented: DummyFilePath.sep")


def child(self, name):
# IFilePath.child
raise NotImplementedError("Unimplemented: DummyFilePath.child")


def changed(self):
# IFilePath.changed
raise NotImplementedError("Unimplemented: DummyFilePath.changed")


def getsize(self):
# IFilePath.getsize
raise NotImplementedError("Unimplemented: DummyFilePath.getsize")


def getModificationTime(self):
# IFilePath.getModificationTime
raise NotImplementedError(
"Unimplemented: DummyFilePath.getModificationTime")

raise NotImplementedError("Unimplemented: DummyFilePath.getModificationTime")

def getStatusChangeTime(self):
# IFilePath.getStatusChangeTime
raise NotImplementedError(
"Unimplemented: DummyFilePath.getStatusChangeTime")

raise NotImplementedError("Unimplemented: DummyFilePath.getStatusChangeTime")

def getAccessTime(self):
# IFilePath.getAccessTime
raise NotImplementedError("Unimplemented: DummyFilePath.getAccessTime")


def isdir(self):
# IFilePath.isdir
raise NotImplementedError("Unimplemented: DummyFilePath.isdir")


def isfile(self):
# IFilePath.isfile
raise NotImplementedError("Unimplemented: DummyFilePath.isfile")


def children(self):
# IFilePath.children
raise NotImplementedError("Unimplemented: DummyFilePath.children")


def basename(self):
# IFilePath.basename
raise NotImplementedError("Unimplemented: DummyFilePath.basename")


def parent(self):
# IFilePath.parent
raise NotImplementedError("Unimplemented: DummyFilePath.parent")


def sibling(self, name):
# IFilePath.sibling
raise NotImplementedError("Unimplemented: DummyFilePath.sibling")
71 changes: 20 additions & 51 deletions src/twisted/application/runner/test/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@
from attr import attrib, attrs, Factory

from twisted.logger import (
LogLevel, LogPublisher, LogBeginner,
FileLogObserver, FilteringLogObserver, LogLevelFilterPredicate,
LogLevel,
LogPublisher,
LogBeginner,
FileLogObserver,
FilteringLogObserver,
LogLevelFilterPredicate,
)
from twisted.internet.testing import MemoryReactor

Expand All @@ -26,7 +30,6 @@
import twisted.trial.unittest



class RunnerTests(twisted.trial.unittest.TestCase):
"""
Tests for L{Runner}.
Expand All @@ -45,7 +48,7 @@ def setUp(self):
# Patch getpid so we get a known result

self.pid = 1337
self.pidFileContent = u"{}\n".format(self.pid).encode("utf-8")
self.pidFileContent = "{}\n".format(self.pid).encode("utf-8")

# Patch globalLogBeginner so that we aren't trying to install multiple
# global log observers.
Expand All @@ -58,14 +61,14 @@ def setUp(self):
self.globalLogPublisher = LogPublisher()
self.globalLogBeginner = LogBeginner(
self.globalLogPublisher,
self.stdio.stderr, self.stdio,
self.stdio.stderr,
self.stdio,
self.warnings,
)

self.patch(_runner, "stderr", self.stderr)
self.patch(_runner, "globalLogBeginner", self.globalLogBeginner)


def test_runInOrder(self):
"""
L{Runner.run} calls the expected methods in order.
Expand All @@ -80,10 +83,9 @@ def test_runInOrder(self):
"startLogging",
"startReactor",
"reactorExited",
]
],
)


def test_runUsesPIDFile(self):
"""
L{Runner.run} uses the provided PID file.
Expand All @@ -100,7 +102,6 @@ def test_runUsesPIDFile(self):
self.assertTrue(pidFile.entered)
self.assertTrue(pidFile.exited)


def test_runAlreadyRunning(self):
"""
L{Runner.run} exits with L{ExitStatus.EX_USAGE} and the expected
Expand All @@ -116,7 +117,6 @@ def test_runAlreadyRunning(self):
self.assertEqual(self.exit.status, ExitStatus.EX_CONFIG)
self.assertEqual(self.exit.message, "Already running.")


def test_killNotRequested(self):
"""
L{Runner.killIfRequested} when C{kill} is false doesn't exit and
Expand All @@ -128,7 +128,6 @@ def test_killNotRequested(self):
self.assertEqual(self.kill.calls, [])
self.assertFalse(self.exit.exited)


def test_killRequestedWithoutPIDFile(self):
"""
L{Runner.killIfRequested} when C{kill} is true but C{pidFile} is
Expand All @@ -142,7 +141,6 @@ def test_killRequestedWithoutPIDFile(self):
self.assertEqual(self.exit.status, ExitStatus.EX_USAGE)
self.assertEqual(self.exit.message, "No PID file specified.")


def test_killRequestedWithPIDFile(self):
"""
L{Runner.killIfRequested} when C{kill} is true and given a C{pidFile}
Expand All @@ -156,7 +154,6 @@ def test_killRequestedWithPIDFile(self):
self.assertEqual(self.exit.status, ExitStatus.EX_OK)
self.assertIdentical(self.exit.message, None)


def test_killRequestedWithPIDFileCantRead(self):
"""
L{Runner.killIfRequested} when C{kill} is true and given a C{pidFile}
Expand All @@ -175,7 +172,6 @@ def read():
self.assertEqual(self.exit.status, ExitStatus.EX_IOERR)
self.assertEqual(self.exit.message, "Unable to read PID file.")


def test_killRequestedWithPIDFileEmpty(self):
"""
L{Runner.killIfRequested} when C{kill} is true and given a C{pidFile}
Expand All @@ -188,7 +184,6 @@ def test_killRequestedWithPIDFileEmpty(self):
self.assertEqual(self.exit.status, ExitStatus.EX_DATAERR)
self.assertEqual(self.exit.message, "Invalid PID file.")


def test_killRequestedWithPIDFileNotAnInt(self):
"""
L{Runner.killIfRequested} when C{kill} is true and given a C{pidFile}
Expand All @@ -201,7 +196,6 @@ def test_killRequestedWithPIDFileNotAnInt(self):
self.assertEqual(self.exit.status, ExitStatus.EX_DATAERR)
self.assertEqual(self.exit.message, "Invalid PID file.")


def test_startLogging(self):
"""
L{Runner.startLogging} sets up a filtering observer with a log level
Expand All @@ -223,8 +217,7 @@ def beginLoggingTo(self, observers):

class MockFilteringLogObserver(FilteringLogObserver):
def __init__(
self, observer, predicates,
negativeObserver=lambda event: None
self, observer, predicates, negativeObserver=lambda event: None
):
MockFilteringLogObserver.observer = observer
MockFilteringLogObserver.predicates = predicates
Expand Down Expand Up @@ -257,24 +250,17 @@ def __init__(self, outFile):
# Check log level predicate with the correct default log level
self.assertEqual(len(MockFilteringLogObserver.predicates), 1)
self.assertIsInstance(
MockFilteringLogObserver.predicates[0],
LogLevelFilterPredicate
MockFilteringLogObserver.predicates[0], LogLevelFilterPredicate
)
self.assertIdentical(
MockFilteringLogObserver.predicates[0].defaultLogLevel,
LogLevel.critical
MockFilteringLogObserver.predicates[0].defaultLogLevel, LogLevel.critical
)

# Check for a file observer attached to the filtering observer
self.assertIsInstance(
MockFilteringLogObserver.observer, MockFileLogObserver
)
self.assertIsInstance(MockFilteringLogObserver.observer, MockFileLogObserver)

# Check for the file we gave it
self.assertIdentical(
MockFilteringLogObserver.observer.outFile, logFile
)

self.assertIdentical(MockFilteringLogObserver.observer.outFile, logFile)

def test_startReactorWithReactor(self):
"""
Expand All @@ -287,31 +273,27 @@ def test_startReactorWithReactor(self):

self.assertTrue(reactor.hasRun)


def test_startReactorWhenRunning(self):
"""
L{Runner.startReactor} ensures that C{whenRunning} is called with
C{whenRunningArguments} when the reactor is running.
"""
self._testHook("whenRunning", "startReactor")


def test_whenRunningWithArguments(self):
"""
L{Runner.whenRunning} calls C{whenRunning} with
C{whenRunningArguments}.
"""
self._testHook("whenRunning")


def test_reactorExitedWithArguments(self):
"""
L{Runner.whenRunning} calls C{reactorExited} with
C{reactorExitedArguments}.
"""
self._testHook("reactorExited")


def _testHook(self, methodName, callerName=None):
"""
Verify that the named hook is run with the expected arguments as
Expand Down Expand Up @@ -349,7 +331,6 @@ def hook(**arguments):
self.assertEqual(argumentsSeen[0], arguments)



@attrs(frozen=True)
class DummyRunner(Runner):
"""
Expand All @@ -360,45 +341,38 @@ class DummyRunner(Runner):

calledMethods = attrib(default=Factory(list))


def killIfRequested(self):
self.calledMethods.append("killIfRequested")


def startLogging(self):
self.calledMethods.append("startLogging")


def startReactor(self):
self.calledMethods.append("startReactor")


def reactorExited(self):
self.calledMethods.append("reactorExited")



class DummyPIDFile(NonePIDFile):
"""
Stub for L{PIDFile}.
Tracks context manager entry/exit without doing anything.
"""

def __init__(self):
NonePIDFile.__init__(self)

self.entered = False
self.exited = False

self.exited = False

def __enter__(self):
self.entered = True
return self


def __exit__(self, excType, excValue, traceback):
self.exited = True

self.exited = True


class DummyExit:
Expand All @@ -410,14 +384,12 @@ class DummyExit:
def __init__(self):
self.exited = False


def __call__(self, status, message=None):
assert not self.exited

self.status = status
self.status = status
self.message = message
self.exited = True

self.exited = True


class DummyKill:
Expand All @@ -429,12 +401,10 @@ class DummyKill:
def __init__(self):
self.calls = []


def __call__(self, pid, sig):
self.calls.append((pid, sig))



class DummyStandardIO:
"""
Stub for L{sys} which provides L{BytesIO} streams as stdout and stderr.
Expand All @@ -445,7 +415,6 @@ def __init__(self, stdout, stderr):
self.stderr = stderr



class DummyWarningsModule:
"""
Stub for L{warnings} which provides a C{showwarning} method that is a no-op.
Expand Down
70 changes: 37 additions & 33 deletions src/twisted/application/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,21 @@ class IServiceMaker(Interface):
L{twisted.plugin.IPlugin}, and will most often be used by the
'twistd' command.
"""

tapname = Attribute(
"A short string naming this Twisted plugin, for example 'web' or "
"'pencil'. This name will be used as the subcommand of 'twistd'.")
"'pencil'. This name will be used as the subcommand of 'twistd'."
)

description = Attribute(
"A brief summary of the features provided by this "
"Twisted application plugin.")
"Twisted application plugin."
)

options = Attribute(
"A C{twisted.python.usage.Options} subclass defining the "
"configuration options for this application.")

"configuration options for this application."
)

def makeService(options):
"""
Expand All @@ -57,19 +60,18 @@ def makeService(options):
"""



@implementer(IPlugin, IServiceMaker)
class ServiceMaker:
"""
Utility class to simplify the definition of L{IServiceMaker} plugins.
"""

def __init__(self, name, module, description, tapname):
self.name = name
self.module = module
self.description = description
self.tapname = tapname


@property
def options(self):
return namedAny(self.module).Options
Expand All @@ -79,22 +81,18 @@ def makeService(self):
return namedAny(self.module).makeService



class IService(Interface):
"""
A service.
Run start-up and shut-down code at the appropriate times.
"""

name = Attribute(
"A C{str} which is the name of the service or C{None}.")
name = Attribute("A C{str} which is the name of the service or C{None}.")

running = Attribute(
"A C{boolean} which indicates whether the service is running.")
running = Attribute("A C{boolean} which indicates whether the service is running.")

parent = Attribute(
"An C{IServiceCollection} which is the parent or C{None}.")
parent = Attribute("An C{IServiceCollection} which is the parent or C{None}.")

def setName(name):
"""
Expand Down Expand Up @@ -152,7 +150,6 @@ def privilegedStartService():
"""



@implementer(IService)
class Service:
"""
Expand All @@ -170,7 +167,7 @@ class Service:
def __getstate__(self):
dict = self.__dict__.copy()
if "running" in dict:
del dict['running']
del dict["running"]
return dict

def setName(self, name):
Expand Down Expand Up @@ -200,7 +197,6 @@ def stopService(self):
self.running = 0



class IServiceCollection(Interface):
"""
Collection of services.
Expand Down Expand Up @@ -253,7 +249,6 @@ def removeService(service):
"""



@implementer(IServiceCollection)
class MultiService(Service):
"""
Expand Down Expand Up @@ -298,8 +293,9 @@ def __iter__(self):
def addService(self, service):
if service.name is not None:
if service.name in self.namedServices:
raise RuntimeError("cannot have two services with same name"
" '%s'" % service.name)
raise RuntimeError(
"cannot have two services with same name" " '%s'" % service.name
)
self.namedServices[service.name] = service
self.services.append(service)
if self.running:
Expand All @@ -319,31 +315,33 @@ def removeService(self, service):
return None



class IProcess(Interface):
"""
Process running parameters.
Represents parameters for how processes should be run.
"""

processName = Attribute(
"""
A C{str} giving the name the process should have in ps (or L{None}
to leave the name alone).
""")
"""
)

uid = Attribute(
"""
An C{int} giving the user id as which the process should run (or
L{None} to leave the UID alone).
""")
"""
)

gid = Attribute(
"""
An C{int} giving the group id as which the process should run (or
L{None} to leave the GID alone).
""")

"""
)


@implementer(IProcess)
Expand All @@ -354,6 +352,7 @@ class Process:
Sets up uid/gid in the constructor, and has a default
of L{None} as C{processName}.
"""

processName = None

def __init__(self, uid=None, gid=None):
Expand All @@ -370,7 +369,6 @@ def __init__(self, uid=None, gid=None):
self.gid = gid



def Application(name, uid=None, gid=None):
"""
Return a compound class.
Expand All @@ -381,16 +379,14 @@ def Application(name, uid=None, gid=None):
one of the interfaces.
"""
ret = components.Componentized()
availableComponents = [MultiService(), Process(uid, gid),
sob.Persistent(ret, name)]
availableComponents = [MultiService(), Process(uid, gid), sob.Persistent(ret, name)]

for comp in availableComponents:
ret.addComponent(comp, ignoreClass=1)
IService(ret).setName(name)
return ret



def loadApplication(filename, kind, passphrase=None):
"""
Load Application from a given file.
Expand All @@ -404,13 +400,21 @@ def loadApplication(filename, kind, passphrase=None):
@type kind: C{str}
@type passphrase: C{str}
"""
if kind == 'python':
application = sob.loadValueFromFile(filename, 'application')
if kind == "python":
application = sob.loadValueFromFile(filename, "application")
else:
application = sob.load(filename, kind)
return application


__all__ = ['IServiceMaker', 'IService', 'Service',
'IServiceCollection', 'MultiService',
'IProcess', 'Process', 'Application', 'loadApplication']
__all__ = [
"IServiceMaker",
"IService",
"Service",
"IServiceCollection",
"MultiService",
"IProcess",
"Process",
"Application",
"loadApplication",
]
10 changes: 5 additions & 5 deletions src/twisted/application/strports.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ def service(description, factory, reactor=None):
from twisted.internet import reactor

svc = StreamServerEndpointService(
endpoints.serverFromString(reactor, description), factory)
endpoints.serverFromString(reactor, description), factory
)
svc._raiseSynchronously = True
return svc



def listen(description, factory):
"""
Listen on a port corresponding to a description.
Expand All @@ -61,9 +61,9 @@ def listen(description, factory):
@see: L{twisted.internet.endpoints.serverFromString}
"""
from twisted.internet import reactor
name, args, kw = endpoints._parseServer(description, factory)
return getattr(reactor, 'listen' + name)(*args, **kw)

name, args, kw = endpoints._parseServer(description, factory)
return getattr(reactor, "listen" + name)(*args, **kw)


__all__ = ['service', 'listen']
__all__ = ["service", "listen"]
199 changes: 81 additions & 118 deletions src/twisted/application/test/test_internet.py

Large diffs are not rendered by default.

26 changes: 5 additions & 21 deletions src/twisted/application/test/test_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ def __init__(self, name, parent, running):
self.parent = parent
self.running = running


def makeInvalidByDeletingName(self):
"""
Probably not a wise method to call.
Expand All @@ -53,7 +52,6 @@ def makeInvalidByDeletingName(self):
"""
del self.name


def makeInvalidByDeletingParent(self):
"""
Probably not a wise method to call.
Expand All @@ -63,7 +61,6 @@ def makeInvalidByDeletingParent(self):
"""
del self.parent


def makeInvalidByDeletingRunning(self):
"""
Probably not a wise method to call.
Expand All @@ -73,59 +70,51 @@ def makeInvalidByDeletingRunning(self):
"""
del self.running


def setName(self, name):
"""
See L{twisted.application.service.IService}.
@param name: ignored
"""


def setServiceParent(self, parent):
"""
See L{twisted.application.service.IService}.
@param parent: ignored
"""


def disownServiceParent(self):
"""
See L{twisted.application.service.IService}.
"""


def privilegedStartService(self):
"""
See L{twisted.application.service.IService}.
"""


def startService(self):
"""
See L{twisted.application.service.IService}.
"""


def stopService(self):
"""
See L{twisted.application.service.IService}.
"""



class ServiceInterfaceTests(TestCase):
"""
Tests for L{twisted.application.service.IService} implementation.
"""

def setUp(self):
"""
Build something that implements IService.
"""
self.almostService = AlmostService(parent=None, running=False,
name=None)

self.almostService = AlmostService(parent=None, running=False, name=None)

def test_realService(self):
"""
Expand All @@ -134,14 +123,12 @@ def test_realService(self):
myService = Service()
verifyObject(IService, myService)


def test_hasAll(self):
"""
AlmostService implements IService.
"""
verifyObject(IService, self.almostService)


def test_noName(self):
"""
AlmostService with no name does not implement IService.
Expand All @@ -150,7 +137,6 @@ def test_noName(self):
with self.assertRaises(BrokenImplementation):
verifyObject(IService, self.almostService)


def test_noParent(self):
"""
AlmostService with no parent does not implement IService.
Expand All @@ -159,7 +145,6 @@ def test_noParent(self):
with self.assertRaises(BrokenImplementation):
verifyObject(IService, self.almostService)


def test_noRunning(self):
"""
AlmostService with no running does not implement IService.
Expand All @@ -169,19 +154,18 @@ def test_noRunning(self):
verifyObject(IService, self.almostService)



class ApplicationTests(TestCase):
"""
Tests for L{twisted.application.service.Application}.
"""

def test_applicationComponents(self):
"""
Check L{twisted.application.service.Application} instantiation.
"""
app = Application('app-name')
app = Application("app-name")

self.assertTrue(verifyObject(IService, IService(app)))
self.assertTrue(
verifyObject(IServiceCollection, IServiceCollection(app)))
self.assertTrue(verifyObject(IServiceCollection, IServiceCollection(app)))
self.assertTrue(verifyObject(IProcess, IProcess(app)))
self.assertTrue(verifyObject(IPersistable, IPersistable(app)))
34 changes: 9 additions & 25 deletions src/twisted/application/twist/_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
from twisted.copyright import version
from twisted.python.usage import Options, UsageError
from twisted.logger import (
LogLevel, InvalidLogLevelError,
textFileLogObserver, jsonFileLogObserver,
LogLevel,
InvalidLogLevelError,
textFileLogObserver,
jsonFileLogObserver,
)
from twisted.plugin import getPlugins

Expand All @@ -24,7 +26,6 @@
openFile = open



class TwistOptions(Options):
"""
Command line options for C{twist}.
Expand All @@ -33,28 +34,22 @@ class TwistOptions(Options):
defaultReactorName = "default"
defaultLogLevel = LogLevel.info


def __init__(self):
Options.__init__(self)

self["reactorName"] = self.defaultReactorName
self["logLevel"] = self.defaultLogLevel
self["logFile"] = stdout


def getSynopsis(self):
return "{} plugin [plugin_options]".format(
Options.getSynopsis(self)
)

return "{} plugin [plugin_options]".format(Options.getSynopsis(self))

def opt_version(self):
"""
Print version and exit.
"""
exit(ExitStatus.EX_OK, "{}".format(version))


def opt_reactor(self, name):
"""
The name of the reactor to use.
Expand All @@ -71,23 +66,20 @@ def opt_reactor(self, name):
self["reactorName"] = name

opt_reactor.__doc__ = dedent(opt_reactor.__doc__ or "").format(
options=", ".join(
'"{}"'.format(rt.shortName) for rt in getReactorTypes()
),
options=", ".join('"{}"'.format(rt.shortName) for rt in getReactorTypes()),
)


def installReactor(self, name):
"""
Install the reactor.
"""
if name == self.defaultReactorName:
from twisted.internet import reactor

return reactor
else:
return installReactor(name)


def opt_log_level(self, levelName):
"""
Set default log level.
Expand All @@ -100,13 +92,11 @@ def opt_log_level(self, levelName):

opt_log_level.__doc__ = dedent(opt_log_level.__doc__ or "").format(
options=", ".join(
'"{}"'.format(constant.name)
for constant in LogLevel.iterconstants()
'"{}"'.format(constant.name) for constant in LogLevel.iterconstants()
),
default=defaultLogLevel.name,
)


def opt_log_file(self, fileName):
"""
Log to file. ("-" for stdout, "+" for stderr; default: "-")
Expand All @@ -124,10 +114,9 @@ def opt_log_file(self, fileName):
except EnvironmentError as e:
exit(
ExitStatus.EX_IOERR,
"Unable to open log file {!r}: {}".format(fileName, e)
"Unable to open log file {!r}: {}".format(fileName, e),
)


def opt_log_format(self, format):
"""
Log file format.
Expand All @@ -146,7 +135,6 @@ def opt_log_format(self, format):

opt_log_format.__doc__ = dedent(opt_log_format.__doc__ or "")


def selectDefaultLogObserver(self):
"""
Set C{fileLogObserverFactory} to the default appropriate for the
Expand All @@ -162,7 +150,6 @@ def selectDefaultLogObserver(self):
self["fileLogObserverFactory"] = jsonFileLogObserver
self["logFormat"] = "json"


def parseOptions(self, options=None):
self.selectDefaultLogObserver()

Expand All @@ -171,7 +158,6 @@ def parseOptions(self, options=None):
if "reactor" not in self:
self["reactor"] = self.installReactor(self["reactorName"])


@property
def plugins(self):
if "plugins" not in self:
Expand All @@ -182,7 +168,6 @@ def plugins(self):

return self["plugins"]


@property
def subCommands(self):
plugins = self.plugins
Expand All @@ -198,7 +183,6 @@ def subCommands(self):
plugin.description,
)


def postOptions(self):
Options.postOptions(self)

Expand Down
9 changes: 1 addition & 8 deletions src/twisted/application/twist/_twist.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from twisted.internet.interfaces import _ISupportsExitSignalCapturing



class Twist:
"""
Run a Twisted application.
Expand All @@ -43,7 +42,6 @@ def options(argv):

return options


@staticmethod
def service(plugin, options):
"""
Expand All @@ -65,7 +63,6 @@ def service(plugin, options):

return IService(application)


@staticmethod
def startService(reactor, service):
"""
Expand All @@ -80,10 +77,7 @@ def startService(reactor, service):
service.startService()

# Ask the reactor to stop the service before shutting down
reactor.addSystemEventTrigger(
"before", "shutdown", service.stopService
)

reactor.addSystemEventTrigger("before", "shutdown", service.stopService)

@staticmethod
def run(twistOptions):
Expand All @@ -106,7 +100,6 @@ def run(twistOptions):
if reactor._exitSignal is not None:
_exitWithSignal(reactor._exitSignal)


@classmethod
def main(cls, argv=sys.argv):
"""
Expand Down
61 changes: 10 additions & 51 deletions src/twisted/application/twist/test/test_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import twisted.trial.unittest



class OptionsTests(twisted.trial.unittest.TestCase):
"""
Tests for L{TwistOptions}.
Expand All @@ -35,7 +34,6 @@ def patchExit(self):
self.exit = DummyExit()
self.patch(_options, "exit", self.exit)


def patchOpen(self):
"""
Patch L{_options.open} so we can capture usage and prevent actual opens.
Expand All @@ -51,7 +49,6 @@ def fakeOpen(name, mode=None):

self.patch(_options, "openFile", fakeOpen)


def patchInstallReactor(self):
"""
Patch C{_options.installReactor} so we can capture usage and prevent
Expand All @@ -69,17 +66,13 @@ def installReactor(name):

self.patch(_options, "installReactor", installReactor)


def test_synopsis(self):
"""
L{TwistOptions.getSynopsis} appends arguments.
"""
options = TwistOptions()

self.assertTrue(
options.getSynopsis().endswith(" plugin [plugin_options]")
)

self.assertTrue(options.getSynopsis().endswith(" plugin [plugin_options]"))

def test_version(self):
"""
Expand All @@ -94,7 +87,6 @@ def test_version(self):
self.assertEquals(self.exit.status, ExitStatus.EX_OK)
self.assertEquals(self.exit.message, version)


def test_reactor(self):
"""
L{TwistOptions.installReactor} installs the chosen reactor and sets
Expand All @@ -108,7 +100,6 @@ def test_reactor(self):
self.assertEqual(set(self.installedReactors), set(["fusion"]))
self.assertEquals(options["reactorName"], "fusion")


def test_installCorrectReactor(self):
"""
L{TwistOptions.installReactor} installs the chosen reactor after the
Expand All @@ -122,7 +113,6 @@ def test_installCorrectReactor(self):

self.assertEqual(set(self.installedReactors), set(["fusion"]))


def test_installReactorBogus(self):
"""
L{TwistOptions.installReactor} raises UsageError if an unknown reactor
Expand All @@ -133,15 +123,13 @@ def test_installReactorBogus(self):
options = TwistOptions()
self.assertRaises(UsageError, options.opt_reactor, "coal")


def test_installReactorDefault(self):
"""
L{TwistOptions.installReactor} returns the currently installed reactor
when the default reactor name is specified.
"""
options = TwistOptions()
self.assertIdentical(reactor, options.installReactor('default'))

self.assertIdentical(reactor, options.installReactor("default"))

def test_logLevelValid(self):
"""
Expand All @@ -152,7 +140,6 @@ def test_logLevelValid(self):

self.assertIdentical(options["logLevel"], LogLevel.warn)


def test_logLevelInvalid(self):
"""
L{TwistOptions.opt_log_level} with an invalid log level name raises
Expand All @@ -162,7 +149,6 @@ def test_logLevelInvalid(self):

self.assertRaises(UsageError, options.opt_log_level, "cheese")


def _testLogFile(self, name, expectedStream):
"""
Set log file name and check the selected output stream.
Expand All @@ -175,21 +161,18 @@ def _testLogFile(self, name, expectedStream):

self.assertIdentical(options["logFile"], expectedStream)


def test_logFileStdout(self):
"""
L{TwistOptions.opt_log_file} given C{"-"} as a file name uses stdout.
"""
self._testLogFile("-", stdout)


def test_logFileStderr(self):
"""
L{TwistOptions.opt_log_file} given C{"+"} as a file name uses stderr.
"""
self._testLogFile("+", stderr)


def test_logFileNamed(self):
"""
L{TwistOptions.opt_log_file} opens the given file name in append mode.
Expand All @@ -201,7 +184,6 @@ def test_logFileNamed(self):

self.assertEqual([("mylog", "a")], self.opened)


def test_logFileCantOpen(self):
"""
L{TwistOptions.opt_log_file} exits with L{ExitStatus.EX_IOERR} if
Expand All @@ -215,12 +197,9 @@ def test_logFileCantOpen(self):

self.assertEquals(self.exit.status, ExitStatus.EX_IOERR)
self.assertTrue(
self.exit.message.startswith(
"Unable to open log file 'nocanopen': "
)
self.exit.message.startswith("Unable to open log file 'nocanopen': ")
)


def _testLogFormat(self, format, expectedObserver):
"""
Set log file format and check the selected observer.
Expand All @@ -231,28 +210,23 @@ def _testLogFormat(self, format, expectedObserver):
options = TwistOptions()
options.opt_log_format(format)

self.assertIdentical(
options["fileLogObserverFactory"], expectedObserver
)
self.assertIdentical(options["fileLogObserverFactory"], expectedObserver)
self.assertEqual(options["logFormat"], format)


def test_logFormatText(self):
"""
L{TwistOptions.opt_log_format} given C{"text"} uses a
L{textFileLogObserver}.
"""
self._testLogFormat("text", textFileLogObserver)


def test_logFormatJSON(self):
"""
L{TwistOptions.opt_log_format} given C{"text"} uses a
L{textFileLogObserver}.
"""
self._testLogFormat("json", jsonFileLogObserver)


def test_logFormatInvalid(self):
"""
L{TwistOptions.opt_log_format} given an invalid format name raises
Expand All @@ -262,7 +236,6 @@ def test_logFormatInvalid(self):

self.assertRaises(UsageError, options.opt_log_format, "frommage")


def test_selectDefaultLogObserverNoOverride(self):
"""
L{TwistOptions.selectDefaultLogObserver} will not override an already
Expand All @@ -272,22 +245,20 @@ def test_selectDefaultLogObserverNoOverride(self):

options = TwistOptions()
options.opt_log_format("text") # Ask for text
options.opt_log_file("queso") # File, not a tty
options.opt_log_file("queso") # File, not a tty
options.selectDefaultLogObserver()

# Because we didn't select a file that is a tty, the default is JSON,
# but since we asked for text, we should get text.
self.assertIdentical(
options["fileLogObserverFactory"], textFileLogObserver
)
self.assertIdentical(options["fileLogObserverFactory"], textFileLogObserver)
self.assertEqual(options["logFormat"], "text")


def test_selectDefaultLogObserverDefaultWithTTY(self):
"""
L{TwistOptions.selectDefaultLogObserver} will not override an already
selected observer.
"""

class TTYFile:
def isatty(self):
return True
Expand All @@ -299,12 +270,9 @@ def isatty(self):
options.opt_log_file("-") # stdout, a tty
options.selectDefaultLogObserver()

self.assertIdentical(
options["fileLogObserverFactory"], textFileLogObserver
)
self.assertIdentical(options["fileLogObserverFactory"], textFileLogObserver)
self.assertEqual(options["logFormat"], "text")


def test_selectDefaultLogObserverDefaultWithoutTTY(self):
"""
L{TwistOptions.selectDefaultLogObserver} will not override an already
Expand All @@ -316,12 +284,9 @@ def test_selectDefaultLogObserverDefaultWithoutTTY(self):
options.opt_log_file("queso") # File, not a tty
options.selectDefaultLogObserver()

self.assertIdentical(
options["fileLogObserverFactory"], jsonFileLogObserver
)
self.assertIdentical(options["fileLogObserverFactory"], jsonFileLogObserver)
self.assertEqual(options["logFormat"], "json")


def test_pluginsType(self):
"""
L{TwistOptions.plugins} is a mapping of available plug-ins.
Expand All @@ -333,7 +298,6 @@ def test_pluginsType(self):
self.assertIsInstance(name, str)
self.assertIsInstance(plugins[name], ServiceMaker)


def test_pluginsIncludeWeb(self):
"""
L{TwistOptions.plugins} includes a C{"web"} plug-in.
Expand All @@ -344,7 +308,6 @@ def test_pluginsIncludeWeb(self):

self.assertIn("web", options.plugins)


def test_subCommandsType(self):
"""
L{TwistOptions.subCommands} is an iterable of tuples as expected by
Expand All @@ -358,21 +321,17 @@ def test_subCommandsType(self):
self.assertTrue(callable(parser))
self.assertIsInstance(doc, str)


def test_subCommandsIncludeWeb(self):
"""
L{TwistOptions.subCommands} includes a sub-command for every plug-in.
"""
options = TwistOptions()

plugins = set(options.plugins)
subCommands = set(
name for name, shortcut, parser, doc in options.subCommands
)
subCommands = set(name for name, shortcut, parser, doc in options.subCommands)

self.assertEqual(subCommands, plugins)


def test_postOptionsNoSubCommand(self):
"""
L{TwistOptions.postOptions} raises L{UsageError} is it has no
Expand Down
Loading